headreel 1.0.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 +21 -0
- package/README.md +174 -0
- package/assets/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/assets/fonts/OFL-JetBrainsMono.txt +93 -0
- package/assets/fonts/OFL-SpaceGrotesk.txt +93 -0
- package/assets/fonts/SpaceGrotesk-Bold.ttf +0 -0
- package/assets/fonts/SpaceGrotesk-Medium.ttf +0 -0
- package/dist/cli/args.js +133 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/run.js +83 -0
- package/dist/core/canvas.js +2 -0
- package/dist/core/data/contributions.js +63 -0
- package/dist/core/data/errors.js +12 -0
- package/dist/core/data/fixture.js +18 -0
- package/dist/core/data/graphql.js +65 -0
- package/dist/core/data/profile.js +13 -0
- package/dist/core/data/token.js +26 -0
- package/dist/core/encode/gif.js +92 -0
- package/dist/core/fonts.js +22 -0
- package/dist/core/pipeline.js +22 -0
- package/dist/core/prng.js +19 -0
- package/dist/core/render/dom.js +66 -0
- package/dist/core/render/render.js +46 -0
- package/dist/styles/contribution-city/city.js +123 -0
- package/dist/styles/contribution-city/index.js +14 -0
- package/dist/styles/contribution-city/options.js +7 -0
- package/dist/styles/contribution-city/sketch.js +289 -0
- package/dist/styles/index.js +5 -0
- package/dist/styles/types.js +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ariful Alam
|
|
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.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# headreel
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/headreel)
|
|
4
|
+
[](https://github.com/arifszn/headreel/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
An animated banner for your GitHub profile, made from your own GitHub activity.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
headreel turns your contributions into a looping GIF. You can use it in two ways:
|
|
12
|
+
|
|
13
|
+
- **One command on your computer.** Run `npx headreel` and get your banner. You do not install or set up anything. It works on macOS, Windows, and Linux.
|
|
14
|
+
- **A GitHub Action that keeps it current.** Add one workflow file, and your banner updates every day by itself.
|
|
15
|
+
|
|
16
|
+
Every style works on both light and dark GitHub themes.
|
|
17
|
+
|
|
18
|
+
## Make your banner now
|
|
19
|
+
|
|
20
|
+
Run this command. Replace `octocat` with your GitHub username.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx headreel --style contribution-city --user octocat
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The command saves your banner as `headreel.gif` in the current folder. The first run downloads headreel. After that, a banner takes a few seconds.
|
|
27
|
+
|
|
28
|
+
Add your tagline and website:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx headreel --style contribution-city --user octocat \
|
|
32
|
+
--tagline "Open source maintainer" \
|
|
33
|
+
--website https://example.com
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
You need [Node.js](https://nodejs.org) 22 (22.22.2 or later), 24 (24.15.0 or later), or 26 and later. You also need a GitHub token. If you use the [GitHub CLI](https://cli.github.com), log in with `gh auth login`, and headreel finds the token. For other ways, see [Command options](#command-options).
|
|
37
|
+
|
|
38
|
+
To put the banner on your profile, add `headreel.gif` to your profile repository. Then add this line to its `README.md`:
|
|
39
|
+
|
|
40
|
+
```markdown
|
|
41
|
+

|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Keep it updated with the GitHub Action
|
|
45
|
+
|
|
46
|
+
A banner that you make on your computer does not change. The Action makes a new banner every day.
|
|
47
|
+
|
|
48
|
+
Your profile repository has the same name as your username, for example `octocat/octocat`. Do these steps in that repository.
|
|
49
|
+
|
|
50
|
+
1. Create the file `.github/workflows/headreel.yml` with this content:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
name: headreel
|
|
54
|
+
|
|
55
|
+
on:
|
|
56
|
+
schedule:
|
|
57
|
+
- cron: '0 0 * * *' # every day at 00:00 UTC
|
|
58
|
+
workflow_dispatch: # lets you run it from the Actions tab
|
|
59
|
+
|
|
60
|
+
permissions:
|
|
61
|
+
contents: write
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
banner:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v7
|
|
68
|
+
- uses: arifszn/headreel@v1
|
|
69
|
+
with:
|
|
70
|
+
style: contribution-city
|
|
71
|
+
tagline: Senior Software Engineer · Distributed Systems
|
|
72
|
+
website: https://example.com
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
2. Open the **Actions** tab of the repository. Select **headreel**, then select **Run workflow**.
|
|
76
|
+
3. Wait for the run to finish. The workflow adds `headreel.gif` to the repository.
|
|
77
|
+
4. Add this line to your `README.md`:
|
|
78
|
+
|
|
79
|
+
```markdown
|
|
80
|
+

|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> [!NOTE]
|
|
84
|
+
> The workflow makes a commit only when the banner changes. Your contribution count changes most days, so expect about one commit each day.
|
|
85
|
+
|
|
86
|
+
## Action inputs
|
|
87
|
+
|
|
88
|
+
| Input | Default | Description |
|
|
89
|
+
| ---------------- | ------------------------------- | ------------------------------------------------------------------ |
|
|
90
|
+
| `style` | (required) | The banner style. See [Styles](#styles). |
|
|
91
|
+
| `username` | repository owner | The GitHub user to show. |
|
|
92
|
+
| `tagline` | empty | One line under your name. Empty means no tagline. |
|
|
93
|
+
| `website` | empty | Your website, for example `https://example.com`. Empty means none. |
|
|
94
|
+
| `handle` | empty | A handle, for styles that show one. |
|
|
95
|
+
| `options` | empty | Style settings, one `key: value` on each line. |
|
|
96
|
+
| `output` | `headreel.gif` | The path of the banner in your repository. |
|
|
97
|
+
| `commit_to` | the checked-out branch | The branch that gets the banner. |
|
|
98
|
+
| `commit_message` | `chore: update headreel banner` | The commit message for each update. |
|
|
99
|
+
| `token` | `github.token` | The token that reads your contribution data. |
|
|
100
|
+
|
|
101
|
+
Your name comes from your GitHub profile. If your profile has no name, the banner shows your username.
|
|
102
|
+
|
|
103
|
+
## Styles
|
|
104
|
+
|
|
105
|
+
### Contribution City
|
|
106
|
+
|
|
107
|
+
`style: contribution-city`
|
|
108
|
+
|
|
109
|
+
Each day of the last 12 months is one building. A taller building means more contributions on that day. A light beam moves across the city, and beacons glow on your busiest days.
|
|
110
|
+
|
|
111
|
+
The banner shows your name, your tagline, your total contributions, and your website.
|
|
112
|
+
|
|
113
|
+
| Option | Default | Description |
|
|
114
|
+
| --------- | ------- | -------------------------------------------------- |
|
|
115
|
+
| `beacons` | `8` | The number of busiest days with a beacon, 0 to 10. |
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
- uses: arifszn/headreel@v1
|
|
121
|
+
with:
|
|
122
|
+
style: contribution-city
|
|
123
|
+
options: |
|
|
124
|
+
beacons: 5
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Command options
|
|
128
|
+
|
|
129
|
+
| Option | Description |
|
|
130
|
+
| ---------------------- | ----------------------------------------------------- |
|
|
131
|
+
| `--style <id>` | The banner style (required). See [Styles](#styles). |
|
|
132
|
+
| `--user <login>` | Your GitHub username (required). |
|
|
133
|
+
| `--out <file>` | Where to save the GIF. The default is `headreel.gif`. |
|
|
134
|
+
| `--tagline <text>` | One line under your name. |
|
|
135
|
+
| `--website <url>` | Your website. |
|
|
136
|
+
| `--handle <text>` | A handle, for styles that show one. |
|
|
137
|
+
| `--option <key=value>` | A style setting. Use it again for each setting. |
|
|
138
|
+
| `--config <file>` | A JSON file with your settings. |
|
|
139
|
+
| `--token <token>` | A GitHub token. See the token order below. |
|
|
140
|
+
|
|
141
|
+
headreel needs a GitHub token to read your contributions. It looks for a token in this order:
|
|
142
|
+
|
|
143
|
+
1. The `--token` option.
|
|
144
|
+
2. The `GITHUB_TOKEN` environment variable.
|
|
145
|
+
3. The [GitHub CLI](https://cli.github.com), if you are logged in with `gh auth login`.
|
|
146
|
+
|
|
147
|
+
You can keep your settings in a JSON file:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"style": "contribution-city",
|
|
152
|
+
"user": "octocat",
|
|
153
|
+
"tagline": "Open source maintainer",
|
|
154
|
+
"website": "https://example.com",
|
|
155
|
+
"options": { "beacons": 5 }
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npx headreel --config headreel.json
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Options on the command line replace the values in the file. To see all options, run `npx headreel --help`.
|
|
164
|
+
|
|
165
|
+
## Troubleshooting
|
|
166
|
+
|
|
167
|
+
**The workflow fails with "Permission denied" or a 403 error.**
|
|
168
|
+
Make sure that the workflow file contains `permissions: contents: write`.
|
|
169
|
+
|
|
170
|
+
**The workflow fails with "Could not resolve to a User".**
|
|
171
|
+
Check the `username` input. It must be a GitHub username.
|
|
172
|
+
|
|
173
|
+
**The command fails with "No GitHub token found".**
|
|
174
|
+
Log in with `gh auth login`, or set the `GITHUB_TOKEN` environment variable.
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
export const USAGE = `Usage: headreel --style <style> --user <login> [options]
|
|
5
|
+
|
|
6
|
+
Renders an animated GitHub profile banner.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
--style <id> Banner style (required). Available: {styles}
|
|
10
|
+
--user <login> GitHub login (required unless --fixture is used)
|
|
11
|
+
--out <file> Output GIF path (default: headreel.gif)
|
|
12
|
+
--tagline <text> One line under your name
|
|
13
|
+
--website <url> Website shown on the banner
|
|
14
|
+
--handle <text> Handle shown on the banner, if the style uses it
|
|
15
|
+
--option <key=value> Style option; repeat for more than one
|
|
16
|
+
--options <text> Style options as "key: value" lines
|
|
17
|
+
--config <file> JSON file with any of the settings above
|
|
18
|
+
--token <token> GitHub token (default: GITHUB_TOKEN, then \`gh auth token\`)
|
|
19
|
+
--fixture <file> Render from a saved data file instead of the GitHub API
|
|
20
|
+
-h, --help Show this help
|
|
21
|
+
-v, --version Show the version`;
|
|
22
|
+
const configSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
style: z.string(),
|
|
25
|
+
user: z.string(),
|
|
26
|
+
out: z.string(),
|
|
27
|
+
tagline: z.string(),
|
|
28
|
+
website: z.string(),
|
|
29
|
+
handle: z.string(),
|
|
30
|
+
fixture: z.string(),
|
|
31
|
+
options: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
|
|
32
|
+
})
|
|
33
|
+
.partial()
|
|
34
|
+
.strict();
|
|
35
|
+
/** Parses "key: value" lines. Blank lines and lines starting with # are ignored. */
|
|
36
|
+
export function parseOptionLines(text) {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const [i, raw] of text.split(/\r?\n/).entries()) {
|
|
39
|
+
const line = raw.trim();
|
|
40
|
+
if (!line || line.startsWith('#'))
|
|
41
|
+
continue;
|
|
42
|
+
const sep = line.indexOf(':');
|
|
43
|
+
if (sep <= 0)
|
|
44
|
+
throw new Error(`Invalid option on line ${i + 1}: "${line}". Use "key: value".`);
|
|
45
|
+
out[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function parseOptionPairs(pairs) {
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const pair of pairs) {
|
|
52
|
+
const sep = pair.indexOf('=');
|
|
53
|
+
if (sep <= 0)
|
|
54
|
+
throw new Error(`Invalid --option "${pair}". Use key=value.`);
|
|
55
|
+
out[pair.slice(0, sep).trim()] = pair.slice(sep + 1).trim();
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
async function readConfig(path) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(await readFile(path, 'utf8'));
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
throw new Error(`Cannot read config ${path}: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
const parsed = configSchema.safeParse(raw);
|
|
68
|
+
if (!parsed.success)
|
|
69
|
+
throw new Error(`Invalid config ${path}:\n${z.prettifyError(parsed.error)}`);
|
|
70
|
+
return parsed.data;
|
|
71
|
+
}
|
|
72
|
+
/** Treats empty strings as unset, so empty Action inputs fall through to defaults. */
|
|
73
|
+
function value(v) {
|
|
74
|
+
const trimmed = v?.trim();
|
|
75
|
+
return trimmed ? trimmed : undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Resolves flags and the optional config file. Flags override config values. */
|
|
78
|
+
export async function parseCommand(argv) {
|
|
79
|
+
const { values } = parseArgs({
|
|
80
|
+
args: argv,
|
|
81
|
+
options: {
|
|
82
|
+
style: { type: 'string' },
|
|
83
|
+
user: { type: 'string' },
|
|
84
|
+
out: { type: 'string' },
|
|
85
|
+
tagline: { type: 'string' },
|
|
86
|
+
website: { type: 'string' },
|
|
87
|
+
handle: { type: 'string' },
|
|
88
|
+
option: { type: 'string', multiple: true },
|
|
89
|
+
options: { type: 'string' },
|
|
90
|
+
config: { type: 'string' },
|
|
91
|
+
token: { type: 'string' },
|
|
92
|
+
fixture: { type: 'string' },
|
|
93
|
+
help: { type: 'boolean', short: 'h' },
|
|
94
|
+
version: { type: 'boolean', short: 'v' },
|
|
95
|
+
},
|
|
96
|
+
strict: true,
|
|
97
|
+
allowPositionals: false,
|
|
98
|
+
});
|
|
99
|
+
if (values.help)
|
|
100
|
+
return { kind: 'help' };
|
|
101
|
+
if (values.version)
|
|
102
|
+
return { kind: 'version' };
|
|
103
|
+
const configPath = value(values.config);
|
|
104
|
+
const config = configPath ? await readConfig(configPath) : {};
|
|
105
|
+
const configOptions = Object.fromEntries(Object.entries(config.options ?? {}).map(([k, v]) => [k, String(v)]));
|
|
106
|
+
const style = value(values.style) ?? config.style;
|
|
107
|
+
if (!style)
|
|
108
|
+
throw new Error('Missing --style.');
|
|
109
|
+
const settings = {
|
|
110
|
+
style,
|
|
111
|
+
out: value(values.out) ?? config.out ?? 'headreel.gif',
|
|
112
|
+
options: {
|
|
113
|
+
...configOptions,
|
|
114
|
+
...parseOptionLines(values.options ?? ''),
|
|
115
|
+
...parseOptionPairs(values.option ?? []),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
const optional = {
|
|
119
|
+
user: value(values.user) ?? config.user,
|
|
120
|
+
tagline: value(values.tagline) ?? config.tagline,
|
|
121
|
+
website: value(values.website) ?? config.website,
|
|
122
|
+
handle: value(values.handle) ?? config.handle,
|
|
123
|
+
token: value(values.token),
|
|
124
|
+
fixture: value(values.fixture) ?? config.fixture,
|
|
125
|
+
};
|
|
126
|
+
for (const [key, v] of Object.entries(optional)) {
|
|
127
|
+
if (v)
|
|
128
|
+
settings[key] = v;
|
|
129
|
+
}
|
|
130
|
+
if (!settings.user && !settings.fixture)
|
|
131
|
+
throw new Error('Missing --user.');
|
|
132
|
+
return { kind: 'render', settings };
|
|
133
|
+
}
|
package/dist/cli/run.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname } from 'node:path';
|
|
3
|
+
import { createGraphQLClient } from '../core/data/graphql.js';
|
|
4
|
+
import { loadFixture } from '../core/data/fixture.js';
|
|
5
|
+
import { fetchProfile } from '../core/data/profile.js';
|
|
6
|
+
import { resolveToken } from '../core/data/token.js';
|
|
7
|
+
import { parseOptions, renderBanner } from '../core/pipeline.js';
|
|
8
|
+
import { styles } from '../styles/index.js';
|
|
9
|
+
import { parseCommand, USAGE } from './args.js';
|
|
10
|
+
const MiB = 1024 * 1024;
|
|
11
|
+
/** GitHub warns on pushed files over 50 MiB and blocks files over 100 MiB. */
|
|
12
|
+
const WARN_BYTES = 50 * MiB;
|
|
13
|
+
const MAX_BYTES = 100 * MiB;
|
|
14
|
+
async function version() {
|
|
15
|
+
const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
16
|
+
return pkg.version;
|
|
17
|
+
}
|
|
18
|
+
async function render(settings, io) {
|
|
19
|
+
const style = styles[settings.style];
|
|
20
|
+
if (!style) {
|
|
21
|
+
throw new Error(`Unknown style "${settings.style}". Available: ${Object.keys(styles).join(', ')}.`);
|
|
22
|
+
}
|
|
23
|
+
if (extname(settings.out).toLowerCase() !== '.gif') {
|
|
24
|
+
throw new Error(`Output must be a .gif file, got "${settings.out}".`);
|
|
25
|
+
}
|
|
26
|
+
// Fail on bad options before any API call.
|
|
27
|
+
parseOptions(style, settings.options);
|
|
28
|
+
let login;
|
|
29
|
+
let name;
|
|
30
|
+
let data;
|
|
31
|
+
if (settings.fixture) {
|
|
32
|
+
const fixture = await loadFixture(settings.fixture, style.data.schema);
|
|
33
|
+
({ login, name } = fixture.profile);
|
|
34
|
+
data = fixture.data;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const client = createGraphQLClient({ token: resolveToken({ flag: settings.token }) });
|
|
38
|
+
const user = settings.user;
|
|
39
|
+
const [profile, fetched] = await Promise.all([
|
|
40
|
+
fetchProfile(client, user),
|
|
41
|
+
style.data.fetch(client, user, io.now()),
|
|
42
|
+
]);
|
|
43
|
+
({ login, name } = profile);
|
|
44
|
+
data = fetched;
|
|
45
|
+
}
|
|
46
|
+
const identity = { name };
|
|
47
|
+
if (settings.tagline)
|
|
48
|
+
identity.tagline = settings.tagline;
|
|
49
|
+
if (settings.website)
|
|
50
|
+
identity.website = settings.website;
|
|
51
|
+
if (settings.handle)
|
|
52
|
+
identity.handle = settings.handle;
|
|
53
|
+
const gif = await renderBanner(style, { login, data, identity, options: settings.options });
|
|
54
|
+
if (gif.length > MAX_BYTES) {
|
|
55
|
+
throw new Error(`Banner is ${(gif.length / MiB).toFixed(1)} MiB; GitHub rejects files over 100 MiB.`);
|
|
56
|
+
}
|
|
57
|
+
if (gif.length > WARN_BYTES) {
|
|
58
|
+
io.warn(`Banner is ${(gif.length / MiB).toFixed(1)} MiB; GitHub warns on files over 50 MiB.`);
|
|
59
|
+
}
|
|
60
|
+
await mkdir(dirname(settings.out), { recursive: true });
|
|
61
|
+
await writeFile(settings.out, gif);
|
|
62
|
+
io.log(`Wrote ${settings.out} (${(gif.length / MiB).toFixed(2)} MiB) for @${login}.`);
|
|
63
|
+
}
|
|
64
|
+
/** Runs the CLI and returns the process exit code. */
|
|
65
|
+
export async function run(argv, io) {
|
|
66
|
+
try {
|
|
67
|
+
const command = await parseCommand(argv);
|
|
68
|
+
if (command.kind === 'help') {
|
|
69
|
+
io.log(USAGE.replace('{styles}', Object.keys(styles).join(', ')));
|
|
70
|
+
}
|
|
71
|
+
else if (command.kind === 'version') {
|
|
72
|
+
io.log(await version());
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await render(command.settings, io);
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
io.warn(`headreel: ${err.message}`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const QUERY = /* GraphQL */ `
|
|
3
|
+
query Contributions($login: String!, $from: DateTime!, $to: DateTime!) {
|
|
4
|
+
user(login: $login) {
|
|
5
|
+
contributionsCollection(from: $from, to: $to) {
|
|
6
|
+
contributionCalendar {
|
|
7
|
+
totalContributions
|
|
8
|
+
weeks {
|
|
9
|
+
contributionDays {
|
|
10
|
+
date
|
|
11
|
+
weekday
|
|
12
|
+
contributionCount
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
`;
|
|
20
|
+
const daySchema = z.object({
|
|
21
|
+
/** YYYY-MM-DD */
|
|
22
|
+
date: z.iso.date(),
|
|
23
|
+
/** 0 = Sunday ... 6 = Saturday */
|
|
24
|
+
weekday: z.number().int().min(0).max(6),
|
|
25
|
+
count: z.number().int().min(0),
|
|
26
|
+
});
|
|
27
|
+
export const contributionsSchema = z.object({
|
|
28
|
+
/** First and last day of the window, YYYY-MM-DD, inclusive. */
|
|
29
|
+
from: z.iso.date(),
|
|
30
|
+
to: z.iso.date(),
|
|
31
|
+
total: z.number().int().min(0),
|
|
32
|
+
/** Calendar columns, oldest first. First and last weeks may be partial. */
|
|
33
|
+
weeks: z.array(z.array(daySchema)),
|
|
34
|
+
});
|
|
35
|
+
const DAY_MS = 86_400_000;
|
|
36
|
+
/**
|
|
37
|
+
* The 365 days ending on `now` (UTC), matching GitHub's profile calendar.
|
|
38
|
+
* GitHub rejects spans over one year, so the window is 364 days back plus today.
|
|
39
|
+
*/
|
|
40
|
+
export function contributionWindow(now) {
|
|
41
|
+
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
42
|
+
return {
|
|
43
|
+
from: new Date(today - 364 * DAY_MS).toISOString().slice(0, 10),
|
|
44
|
+
to: new Date(today).toISOString().slice(0, 10),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function fetchContributions(client, login, now) {
|
|
48
|
+
const { from, to } = contributionWindow(now);
|
|
49
|
+
const { user } = await client(QUERY, {
|
|
50
|
+
login,
|
|
51
|
+
from: `${from}T00:00:00Z`,
|
|
52
|
+
to: `${to}T23:59:59Z`,
|
|
53
|
+
});
|
|
54
|
+
const calendar = user.contributionsCollection.contributionCalendar;
|
|
55
|
+
return {
|
|
56
|
+
from,
|
|
57
|
+
to,
|
|
58
|
+
total: calendar.totalContributions,
|
|
59
|
+
weeks: calendar.weeks.map((week) => week.contributionDays
|
|
60
|
+
.map((d) => ({ date: d.date, weekday: d.weekday, count: d.contributionCount }))
|
|
61
|
+
.sort((a, b) => a.date.localeCompare(b.date))),
|
|
62
|
+
};
|
|
63
|
+
}
|