repo-timelapse 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 0scar07
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.es.md ADDED
@@ -0,0 +1,145 @@
1
+ <div align="center">
2
+
3
+ # repo timelapse
4
+
5
+ **Mira crecer tu repositorio, en video.**
6
+
7
+ Un comando convierte tu historial de git en un timelapse: las carpetas se ramifican, los archivos se encienden al nacer, las personas saltan a lo que tocan y cada release importante tiene su momento. Con banda sonora propia.
8
+
9
+ [![npm](https://img.shields.io/npm/v/repo-timelapse?color=9D8CFF&label=npm)](https://www.npmjs.com/package/repo-timelapse)
10
+ [![CI](https://github.com/0scar07/repo-timelapse/actions/workflows/ci.yml/badge.svg)](https://github.com/0scar07/repo-timelapse/actions/workflows/ci.yml)
11
+ [![licencia](https://img.shields.io/badge/licencia-MIT-9D8CFF)](LICENSE)
12
+
13
+ [English](README.md)
14
+
15
+ <img src="docs/demo-es.gif" alt="Demo de repo timelapse: un árbol radial de carpetas y archivos que crece con el tiempo, con personas, una tarjeta de hito y un resumen final" width="720">
16
+
17
+ <sub>Datos de ejemplo. Ejemplos reales con sonido (como <b>expressjs/express</b>, 17 años en menos de un minuto) en <a href="https://github.com/0scar07/repo-timelapse/releases">Releases</a>.</sub>
18
+
19
+ </div>
20
+
21
+ ## Empieza
22
+
23
+ ```bash
24
+ npx repo-timelapse
25
+ ```
26
+
27
+ Ejecútalo dentro de cualquier repositorio de git. Obtienes `<repo>-timelapse.mp4`.
28
+
29
+ También sirve con cualquier repo público de GitHub, sin clonarlo tú:
30
+
31
+ ```bash
32
+ npx repo-timelapse --github expressjs/express
33
+ ```
34
+
35
+ Solo descarga la historia (commits y estructura de carpetas), no el contenido de los archivos, así que hasta los repos enormes van rápido.
36
+
37
+ **Requisitos:** Node.js 18.17+ y git. La primera vez tarda un par de minutos: npx instala el renderizador y descarga un Chrome sin interfaz (solo una vez).
38
+
39
+ ## Qué vas a ver
40
+
41
+ | Momento | Qué pasa |
42
+ |---|---|
43
+ | Intro | El nombre del repo, el día que empezó y su primer commit, como una semilla |
44
+ | Crecimiento | Las carpetas son ramas y los archivos luces, con el color de su lenguaje. Las carpetas principales llevan su nombre. Los archivos nuevos brillan, los editados destellan y los borrados se apagan en rojo. La cámara se aleja a medida que el árbol crece |
45
+ | Personas | Quienes más aportan saltan a los archivos que tocan, con un rayo punteado. En repos de GitHub aparecen con su foto de perfil real |
46
+ | Hitos | En cada release importante (`v1.0.0`, `2.0.0`…) o número redondo de commits, el tiempo se frena y una tarjeta muestra cuánto había crecido el proyecto |
47
+ | Marcadores | Contadores en vivo de commits, archivos y personas, el último commit y una línea de tiempo con los hitos |
48
+ | Cierre | El árbol completo y el resumen: cuánto tiempo, cuántos commits, personas, archivos y días activos |
49
+
50
+ La duración se adapta a la historia: unos 25 segundos para repos chicos y alrededor de un minuto para los grandes. Los hitos salen solos de tus tags.
51
+
52
+ ## En tu README, en cada release
53
+
54
+ Crea `.github/workflows/timelapse.yml` y tu README mostrará cómo creció el proyecto, actualizado en cada release (el MP4 también se adjunta a la release):
55
+
56
+ ```yaml
57
+ name: timelapse
58
+ on:
59
+ release:
60
+ types: [published]
61
+ workflow_dispatch:
62
+
63
+ permissions:
64
+ contents: write
65
+
66
+ jobs:
67
+ timelapse:
68
+ runs-on: ubuntu-latest
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ with:
72
+ fetch-depth: 0 # toda la historia
73
+ ref: ${{ github.event.repository.default_branch }}
74
+ - uses: 0scar07/repo-timelapse@v1
75
+ with:
76
+ lang: es
77
+ ```
78
+
79
+ Pon estas dos líneas donde quieras el GIF; si no, va al final:
80
+
81
+ ```md
82
+ <!-- repo-timelapse:start -->
83
+ <!-- repo-timelapse:end -->
84
+ ```
85
+
86
+ | Entrada | Por defecto | Qué hace |
87
+ |---|---|---|
88
+ | `title` | nombre del repo | Nombre que se muestra |
89
+ | `lang` | `en` | `en` o `es` |
90
+ | `readme` | `README.md` | README a actualizar (vacío para omitirlo) |
91
+ | `output-dir` | `.github/timelapse` | Dónde se guardan el MP4, el GIF y los datos |
92
+ | `vertical` | `false` | Genera también el video 9:16 |
93
+ | `release-asset` | `true` | Adjunta el MP4 a la release que lo disparó |
94
+ | `commit` | `true` | Hace commit y push a la rama principal |
95
+
96
+ ## Opciones
97
+
98
+ | Opción | Qué hace |
99
+ |---|---|
100
+ | `--github <owner/repo>` | Un repo de GitHub en vez de una carpeta local |
101
+ | `--token <token>` | Token de GitHub (o `GITHUB_TOKEN`) para repos privados |
102
+ | `--since <fecha>` / `--until <fecha>` | Solo una parte de la historia, ej. `--since 2024-01-01` |
103
+ | `--title <texto>` | Nombre que se muestra (por defecto, el de la carpeta o el repo) |
104
+ | `--lang es\|en` | Idioma del video (por defecto, el de tu sistema) |
105
+ | `--vertical` | Video vertical 9:16 para TikTok, Reels y Shorts |
106
+ | `--fps 30\|60` | Fotogramas por segundo (30 por defecto) |
107
+ | `--no-music` | Video sin audio |
108
+ | `--no-avatars` | Iniciales en vez de las fotos de perfil de GitHub |
109
+ | `--gif` | Genera también un GIF liviano (para READMEs) |
110
+ | `--readme <archivo>` | Pone el GIF en ese README, entre marcadores |
111
+ | `--gl <modo>` | Renderizado de Chrome: `angle` (GPU, por defecto en Windows y macOS), `swangle`, `default` |
112
+ | `-o, --out <archivo>` | Archivo de salida |
113
+ | `--json` | Solo imprime los datos, sin renderizar |
114
+ | `--data <archivo>` | Renderiza desde un JSON guardado |
115
+ | `--demo` | Renderiza con datos de ejemplo |
116
+
117
+ **Repos grandes:** si hay más de ~1.400 archivos, las carpetas más profundas se agrupan en racimos para que el video siga fluido. Los bots (dependabot, renovate…) cuentan en los totales pero no salen como personas.
118
+
119
+ ## Cómo funciona
120
+
121
+ 1. `git log --name-status` da cada commit con los archivos que añadió, cambió o borró. No se calculan diffs, por eso es rápido.
122
+ 2. El árbol de carpetas se dibuja en forma radial: el ángulo según la cantidad de archivos y el radio según la profundidad.
123
+ 3. Cada fotograma del video es una función pura del tiempo ([Remotion](https://www.remotion.dev)), así que los renders son deterministas.
124
+ 4. La banda sonora se sintetiza para cada video (120 BPM, original, MIT): el ritmo se corta en cada hito y el cierre tiene un brillo.
125
+
126
+ ## Desarrollo
127
+
128
+ ```bash
129
+ npm install
130
+ npm run studio # preview en vivo en Remotion Studio con los datos de ejemplo
131
+ npm run demo # renderiza el video de ejemplo
132
+ npm test
133
+ ```
134
+
135
+ Las contribuciones son bienvenidas. Mira [CONTRIBUTING.md](CONTRIBUTING.md).
136
+
137
+ ## De la misma familia
138
+
139
+ [git wrapped](https://github.com/0scar07/git-wrapped): tu año en git en un video de 18 segundos.
140
+
141
+ Inspirado en [Gource](https://github.com/acaudwell/Gource), el visualizador de repositorios clásico.
142
+
143
+ ## Licencia
144
+
145
+ [MIT](LICENSE)
package/README.md CHANGED
@@ -1,3 +1,143 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # repo timelapse
4
+
5
+ **Watch your repository grow, as a video.**
6
+
7
+ One command turns your git history into a timelapse: folders branch out, files light up as they're born, contributors jump to what they touch, and every big release gets its moment. With its own soundtrack.
8
+
9
+ [![npm](https://img.shields.io/npm/v/repo-timelapse?color=9D8CFF&label=npm)](https://www.npmjs.com/package/repo-timelapse)
10
+ [![CI](https://github.com/0scar07/repo-timelapse/actions/workflows/ci.yml/badge.svg)](https://github.com/0scar07/repo-timelapse/actions/workflows/ci.yml)
11
+ [![license](https://img.shields.io/badge/license-MIT-9D8CFF)](LICENSE)
12
+
13
+ [Español](README.es.md)
14
+
15
+ <img src="docs/demo-en.gif" alt="repo timelapse demo: a radial tree of folders and files growing over time, with contributors, a milestone card and a closing summary" width="720">
16
+
17
+ <sub>Sample data. Real examples with sound (like <b>expressjs/express</b>, 17 years in under a minute) in <a href="https://github.com/0scar07/repo-timelapse/releases">Releases</a>.</sub>
18
+
19
+ </div>
20
+
21
+ ## Quick start
22
+
23
+ ```bash
24
+ npx repo-timelapse
25
+ ```
26
+
27
+ Run it inside any git repository. You get `<repo>-timelapse.mp4`.
28
+
29
+ Any public GitHub repo works too, without cloning it yourself:
30
+
31
+ ```bash
32
+ npx repo-timelapse --github expressjs/express
33
+ ```
34
+
35
+ It only downloads the history (commits and folder structure), not the file contents, so even huge repos are fast.
36
+
37
+ **Requirements:** Node.js 18.17+ and git. The first run takes a couple of minutes: npx installs the renderer and downloads a headless Chrome (only once).
38
+
39
+ ## What you'll see
40
+
41
+ | Moment | What happens |
42
+ |---|---|
43
+ | Intro | The repo name, the day it started and its first commit, as a seed |
44
+ | Growth | Folders are branches and files are lights, colored by language. The main folders are labeled. New files glow, edited files flash, deleted ones fade out in red. The camera pulls back as the tree grows |
45
+ | Contributors | The top contributors jump to the files they touch, with a dotted beam. On GitHub repos they show their real profile photo |
46
+ | Milestones | At each big release (`v1.0.0`, `2.0.0`…) or round number of commits, time stops and a card shows how far the project had come |
47
+ | HUD | Live counters of commits, files and people, the latest commit, and a timeline with every milestone |
48
+ | Ending | The full tree and a summary: how long, how many commits, people, files and active days |
49
+
50
+ The video length adapts to the history: from about 25 seconds for small repos to about a minute for big ones. Milestones are picked automatically from your tags.
51
+
52
+ ## In your README, on every release
53
+
54
+ Add `.github/workflows/timelapse.yml` and your README will show how the project grew, updated on each release (the MP4 is also attached to the release):
55
+
56
+ ```yaml
57
+ name: timelapse
58
+ on:
59
+ release:
60
+ types: [published]
61
+ workflow_dispatch:
62
+
63
+ permissions:
64
+ contents: write
65
+
66
+ jobs:
67
+ timelapse:
68
+ runs-on: ubuntu-latest
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ with:
72
+ fetch-depth: 0 # the whole history
73
+ ref: ${{ github.event.repository.default_branch }}
74
+ - uses: 0scar07/repo-timelapse@v1
75
+ ```
76
+
77
+ Put these two lines where you want the GIF; otherwise it goes at the end:
78
+
79
+ ```md
80
+ <!-- repo-timelapse:start -->
81
+ <!-- repo-timelapse:end -->
82
+ ```
83
+
84
+ | Input | Default | What it does |
85
+ |---|---|---|
86
+ | `title` | repo name | Name shown in the video |
87
+ | `lang` | `en` | `en` or `es` |
88
+ | `readme` | `README.md` | README to update (empty to skip) |
89
+ | `output-dir` | `.github/timelapse` | Where the MP4, GIF and data go |
90
+ | `vertical` | `false` | Also render a 9:16 video |
91
+ | `release-asset` | `true` | Attach the MP4 to the release that triggered it |
92
+ | `commit` | `true` | Commit and push to the default branch |
93
+
94
+ ## Options
95
+
96
+ | Flag | What it does |
97
+ |---|---|
98
+ | `--github <owner/repo>` | A GitHub repo instead of a local folder |
99
+ | `--token <token>` | GitHub token (or `GITHUB_TOKEN`) for private repos |
100
+ | `--since <date>` / `--until <date>` | Only part of the history, e.g. `--since 2024-01-01` |
101
+ | `--title <text>` | Name shown in the video (default: folder or repo name) |
102
+ | `--lang es\|en` | Video language (default: your system language) |
103
+ | `--vertical` | Vertical 9:16 video for TikTok, Reels and Shorts |
104
+ | `--fps 30\|60` | Frame rate (default 30) |
105
+ | `--no-music` | Silent video |
106
+ | `--no-avatars` | Initials instead of GitHub profile photos |
107
+ | `--gif` | Also make a light GIF (for READMEs) |
108
+ | `--readme <file>` | Put the GIF into that README, between markers |
109
+ | `--gl <mode>` | Chrome rendering: `angle` (GPU, default on Windows and macOS), `swangle`, `default` |
110
+ | `-o, --out <file>` | Output file |
111
+ | `--json` | Print the computed data and skip rendering |
112
+ | `--data <file>` | Render from a saved JSON |
113
+ | `--demo` | Render with sample data |
114
+
115
+ **Big repos:** if there are more than ~1,400 files, the deepest folders are grouped into clusters so the video stays smooth. Bots (dependabot, renovate…) count in the totals but don't appear as contributors.
116
+
117
+ ## How it works
118
+
119
+ 1. `git log --name-status` gives every commit with the files it added, changed or deleted. No diffs are computed, which keeps it fast.
120
+ 2. The folder tree is laid out radially: angle by number of files, radius by depth.
121
+ 3. Each frame of the video is a pure function of time ([Remotion](https://www.remotion.dev)), so renders are deterministic.
122
+ 4. The soundtrack is synthesized for each video (120 BPM, original, MIT): the beat drops at every milestone and the ending gets a shimmer.
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ npm install
128
+ npm run studio # live preview in Remotion Studio with the sample data
129
+ npm run demo # render the sample video
130
+ npm test
131
+ ```
132
+
133
+ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
134
+
135
+ ## Related
136
+
137
+ From the same family: [git wrapped](https://github.com/0scar07/git-wrapped), your year in git as an 18-second video.
138
+
139
+ Inspired by [Gource](https://github.com/acaudwell/Gource), the classic repository visualizer.
140
+
141
+ ## License
142
+
143
+ [MIT](LICENSE)
package/bin/cli.mjs ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ // repo-timelapse: mira crecer tu repo en un video.
3
+ import { parseArgs } from 'node:util';
4
+ import { execFileSync } from 'node:child_process';
5
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { analyzeRepo } from '../src/history.mjs';
10
+ import { makeTimeline } from '../src/timing.mjs';
11
+
12
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
+ const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
14
+
15
+ const { values: o, positionals } = parseArgs({
16
+ allowPositionals: true,
17
+ options: {
18
+ github: { type: 'string' },
19
+ token: { type: 'string' },
20
+ since: { type: 'string' },
21
+ until: { type: 'string' },
22
+ title: { type: 'string' },
23
+ lang: { type: 'string' },
24
+ vertical: { type: 'boolean' },
25
+ fps: { type: 'string' },
26
+ 'no-music': { type: 'boolean' },
27
+ 'no-avatars': { type: 'boolean' },
28
+ gif: { type: 'boolean' },
29
+ readme: { type: 'string' },
30
+ gl: { type: 'string' },
31
+ out: { type: 'string', short: 'o' },
32
+ json: { type: 'boolean' },
33
+ data: { type: 'string' },
34
+ demo: { type: 'boolean' },
35
+ help: { type: 'boolean', short: 'h' },
36
+ version: { type: 'boolean', short: 'v' },
37
+ },
38
+ });
39
+
40
+ const sysLang = (Intl.DateTimeFormat().resolvedOptions().locale || 'en').toLowerCase().startsWith('es') ? 'es' : 'en';
41
+ const lang = o.lang === 'es' || o.lang === 'en' ? o.lang : sysLang;
42
+ const es = lang === 'es';
43
+ const say = (e, n) => console.log(es ? e : n);
44
+ const log = o.json ? () => {} : say;
45
+ const fail = (e, n) => { say(e, n); process.exit(1); };
46
+
47
+ if (o.version) { console.log(pkg.version); process.exit(0); }
48
+ if (o.help) {
49
+ console.log(`
50
+ repo-timelapse ${pkg.version}
51
+
52
+ ${es ? 'Uso' : 'Usage'}: npx repo-timelapse [${es ? 'ruta' : 'path'}] [${es ? 'opciones' : 'options'}]
53
+
54
+ --github <owner/repo> ${es ? 'Un repo público de GitHub (se clona sin el contenido de los archivos, rápido)' : 'A public GitHub repo (cloned without file contents, fast)'}
55
+ --token <token> ${es ? 'Token de GitHub (o GITHUB_TOKEN) para repos privados' : 'GitHub token (or GITHUB_TOKEN) for private repos'}
56
+ --since <fecha> ${es ? 'Empieza en esa fecha (ej.: 2024-01-01)' : 'Start at that date (e.g. 2024-01-01)'}
57
+ --until <fecha> ${es ? 'Termina en esa fecha' : 'End at that date'}
58
+ --title <texto> ${es ? 'Nombre que se muestra (por defecto, el de la carpeta)' : 'Name shown (default: the folder name)'}
59
+ --lang es|en ${es ? 'Idioma del video' : 'Video language'}
60
+ --vertical ${es ? 'Formato vertical 9:16 para TikTok, Reels y Shorts' : 'Vertical 9:16 for TikTok, Reels and Shorts'}
61
+ --fps 30|60 ${es ? 'Fotogramas por segundo (30 por defecto)' : 'Frames per second (default 30)'}
62
+ --no-music ${es ? 'Video sin audio' : 'Silent video'}
63
+ --no-avatars ${es ? 'Iniciales en vez de las fotos de GitHub' : 'Initials instead of GitHub photos'}
64
+ --gif ${es ? 'Genera también un GIF (para READMEs)' : 'Also make a GIF (for READMEs)'}
65
+ --readme <archivo> ${es ? 'Pone el GIF en ese README, entre marcadores' : 'Put the GIF into that README, between markers'}
66
+ --gl <modo> ${es ? 'Renderizado de Chrome: angle (GPU), swangle, default…' : 'Chrome rendering: angle (GPU), swangle, default…'}
67
+ -o, --out <archivo> ${es ? 'Archivo de salida' : 'Output file'}
68
+ --json ${es ? 'Solo imprime los datos, sin renderizar' : 'Print the data only, no render'}
69
+ --data <json> ${es ? 'Renderiza desde un JSON ya generado' : 'Render from a saved JSON'}
70
+ --demo ${es ? 'Renderiza con datos de ejemplo' : 'Render with sample data'}
71
+ `);
72
+ process.exit(0);
73
+ }
74
+
75
+ const git = (args, opts = {}) => execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...opts });
76
+ let data, cleanup = null;
77
+
78
+ if (o.demo) {
79
+ data = { ...JSON.parse(readFileSync(path.join(root, 'src/demo.json'), 'utf8')), lang };
80
+ } else if (o.data) {
81
+ data = { ...JSON.parse(readFileSync(path.resolve(o.data), 'utf8')), ...(o.lang ? { lang } : {}) };
82
+ } else {
83
+ let repo = path.resolve(positionals[0] || '.');
84
+ let name = o.title;
85
+ if (o.github) {
86
+ const slug = o.github.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '').replace(/\/$/, '');
87
+ if (!/^[\w.-]+\/[\w.-]+$/.test(slug)) fail('Usa --github owner/repo (ej.: --github vercel/next.js)', 'Use --github owner/repo (e.g. --github vercel/next.js)');
88
+ const token = o.token || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '';
89
+ const dir = mkdtempSync(path.join(tmpdir(), 'repo-timelapse-'));
90
+ cleanup = () => rmSync(dir, { recursive: true, force: true });
91
+ const auth = token ? ['-c', `http.https://github.com/.extraheader=AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}`] : [];
92
+ log(`Clonando ${slug} (solo la historia, sin el contenido de los archivos)…`, `Cloning ${slug} (history only, no file contents)…`);
93
+ try {
94
+ git([...auth, 'clone', '--quiet', '--no-checkout', '--filter=blob:none', `https://github.com/${slug}.git`, dir], { env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
95
+ } catch {
96
+ cleanup();
97
+ fail(`No pude clonar ${slug}. ¿Existe y es público? Para privados usa --token.`, `Couldn't clone ${slug}. Does it exist and is it public? For private repos use --token.`);
98
+ }
99
+ repo = dir;
100
+ name = name || slug.split('/')[1];
101
+ } else {
102
+ try { repo = git(['-C', repo, 'rev-parse', '--show-toplevel']).trim(); }
103
+ catch { fail('Aquí no hay un repo de git. Pasa una ruta o usa --github owner/repo.', 'No git repo here. Pass a path or use --github owner/repo.'); }
104
+ }
105
+ log('Leyendo la historia…', 'Reading the history…');
106
+ data = analyzeRepo(repo, { since: o.since, until: o.until, name, lang });
107
+ if (data) {
108
+ const { githubSlug } = await import('../src/github.mjs');
109
+ data.slug = githubSlug(repo, o.github ? o.github.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '').replace(/\/$/, '') : null);
110
+ }
111
+ cleanup?.();
112
+ if (!data) fail('Ese repo no tiene commits (o ninguno en ese rango de fechas).', 'That repo has no commits (or none in that date range).');
113
+ }
114
+
115
+ if (o.json) { console.log(JSON.stringify(data)); process.exit(0); }
116
+
117
+ const tl = makeTimeline(data);
118
+ say(`${data.totals.commits} commits · ${data.totals.authors} ${es ? 'personas' : 'people'} · ${data.milestones.length} ${es ? 'hitos' : 'milestones'} · ${tl.total.toFixed(0)} s`,
119
+ `${data.totals.commits} commits · ${data.totals.authors} people · ${data.milestones.length} milestones · ${tl.total.toFixed(0)}s`);
120
+ if (data.collapsed) say('(Repo grande: las carpetas más profundas se agrupan para que el video sea fluido.)', '(Big repo: the deepest folders are grouped so the video stays smooth.)');
121
+
122
+ const fps = o.fps === '60' ? 60 : 30;
123
+ const safe = data.name.replace(/[^\w.-]+/g, '-');
124
+ const out = path.resolve(o.out || `${safe}-timelapse${o.vertical ? '-vertical' : ''}.mp4`);
125
+ mkdirSync(path.dirname(out), { recursive: true });
126
+ const publicDir = mkdtempSync(path.join(tmpdir(), 'repo-timelapse-pub-'));
127
+ let music = null;
128
+ if (!o['no-music']) {
129
+ const { synthMusic } = await import('../src/music.mjs');
130
+ writeFileSync(path.join(publicDir, 'music.wav'), synthMusic(tl));
131
+ music = 'music.wav';
132
+ }
133
+ // fotos de perfil de GitHub para los avatares (si el repo es de GitHub)
134
+ if (data.slug && !o['no-avatars'] && data.authors.length) {
135
+ const { fetchAvatars } = await import('../src/github.mjs');
136
+ const files = await fetchAvatars({ slug: data.slug, authors: data.authors, token: o.token || process.env.GITHUB_TOKEN || process.env.GH_TOKEN || '', outDir: publicDir });
137
+ data.authors.forEach((a, i) => { a.avatar = files[i]; });
138
+ const got = files.filter(Boolean).length;
139
+ if (got) say(`Fotos de GitHub: ${got}/${files.length}`, `GitHub photos: ${got}/${files.length}`);
140
+ }
141
+
142
+ const { bundle } = await import('@remotion/bundler');
143
+ const { renderMedia, selectComposition } = await import('@remotion/renderer');
144
+ say('Preparando el video…', 'Preparing the video…');
145
+ const serveUrl = await bundle({ entryPoint: path.join(root, 'src/remotion/index.ts'), publicDir });
146
+ const inputProps = { data, fps, music, vertical: Boolean(o.vertical) };
147
+ const composition = await selectComposition({ serveUrl, id: 'Timelapse', inputProps });
148
+ let gl = o.gl ?? (process.platform === 'linux' ? null : 'angle');
149
+ const t0 = Date.now();
150
+ const render = () => {
151
+ let last = -1;
152
+ return renderMedia({
153
+ composition, serveUrl, inputProps, codec: 'h264', outputLocation: out, crf: 18,
154
+ chromiumOptions: gl && gl !== 'default' ? { gl } : {},
155
+ onProgress: ({ progress }) => { const p = Math.floor(progress * 100); if (p !== last && p % 5 === 0) { last = p; process.stdout.write(`\r${es ? 'Renderizando' : 'Rendering'} ${p}% `); } },
156
+ });
157
+ };
158
+ try { await render(); } catch (err) {
159
+ if (!gl || gl === 'default') throw err;
160
+ say('\nLa aceleración por GPU falló; reintento sin ella…', '\nGPU acceleration failed; retrying without it…');
161
+ gl = null; await render();
162
+ }
163
+ console.log('');
164
+ const base = out.replace(/\.mp4$/i, '');
165
+ if (o.gif || o.readme) {
166
+ // GIF liviano para READMEs: 10 fps y ~576 px de ancho
167
+ let last = -1;
168
+ await renderMedia({
169
+ composition, serveUrl, inputProps, codec: 'gif', outputLocation: base + '.gif', everyNthFrame: fps / 10, scale: o.vertical ? 0.4 : 0.3, muted: true,
170
+ chromiumOptions: gl && gl !== 'default' ? { gl } : {},
171
+ onProgress: ({ progress }) => { const p = Math.floor(progress * 100); if (p !== last && p % 10 === 0) { last = p; process.stdout.write(`\rGIF ${p}% `); } },
172
+ });
173
+ console.log('');
174
+ say(`GIF: ${base}.gif`, `GIF: ${base}.gif`);
175
+ }
176
+ if (o.readme) {
177
+ const { updateReadme } = await import('../src/readme.mjs');
178
+ updateReadme({ readme: path.resolve(o.readme), image: base + '.gif', video: out, lang, name: data.name });
179
+ say(`README actualizado: ${path.resolve(o.readme)}`, `README updated: ${path.resolve(o.readme)}`);
180
+ }
181
+ rmSync(publicDir, { recursive: true, force: true });
182
+ const secs = Math.round((Date.now() - t0) / 1000);
183
+ say(`Listo en ${secs} s: ${out}\nCompártelo con #RepoTimelapse`, `Done in ${secs}s: ${out}\nShare it with #RepoTimelapse`);
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "repo-timelapse",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Watch your git repository grow: a timelapse video of its history, with one command. Mira crecer tu repo en un video.",
5
+ "type": "module",
6
+ "bin": {
7
+ "repo-timelapse": "bin/cli.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "assets"
13
+ ],
14
+ "scripts": {
15
+ "studio": "remotion studio src/remotion/index.ts",
16
+ "demo": "node bin/cli.mjs --demo",
17
+ "test": "node --test tests/history.test.mjs",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "keywords": [
21
+ "git",
22
+ "timelapse",
23
+ "gource",
24
+ "visualization",
25
+ "remotion",
26
+ "video",
27
+ "cli",
28
+ "history",
29
+ "data-visualization"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/0scar07/repo-timelapse.git"
34
+ },
35
+ "homepage": "https://github.com/0scar07/repo-timelapse#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/0scar07/repo-timelapse/issues"
38
+ },
39
+ "author": "0scar07",
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=18.17"
43
+ },
44
+ "dependencies": {
45
+ "@remotion/bundler": "4.0.543",
46
+ "@remotion/cli": "4.0.543",
47
+ "@remotion/google-fonts": "4.0.543",
48
+ "@remotion/layout-utils": "4.0.543",
49
+ "@remotion/renderer": "4.0.543",
50
+ "react": "19.3.1",
51
+ "react-dom": "19.3.1",
52
+ "remotion": "4.0.543"
53
+ },
54
+ "devDependencies": {
55
+ "@types/react": "^19.3.0",
56
+ "typescript": "^7.0.2"
57
+ }
58
+ }