ghost-github-portfolio 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 +674 -0
- package/README.md +210 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +116 -0
- package/dist/config.js.map +1 -0
- package/dist/generator.d.ts +5 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +204 -0
- package/dist/generator.js.map +1 -0
- package/dist/ghost.d.ts +12 -0
- package/dist/ghost.d.ts.map +1 -0
- package/dist/ghost.js +71 -0
- package/dist/ghost.js.map +1 -0
- package/dist/github.d.ts +9 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +107 -0
- package/dist/github.js.map +1 -0
- package/dist/http.d.ts +14 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +64 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/images/banner.svg" alt="Ghost GitHub Portfolio banner" width="900"/>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/ghost-github-portfolio"><img src="https://img.shields.io/npm/v/ghost-github-portfolio?style=flat-square&logo=npm" alt="npm"></a>
|
|
7
|
+
<a href="https://hub.docker.com/r/drumsergio/ghost-github-portfolio"><img src="https://img.shields.io/docker/pulls/drumsergio/ghost-github-portfolio?style=flat-square&logo=docker" alt="Docker Pulls"></a>
|
|
8
|
+
<a href="https://github.com/GeiserX/ghost-github-portfolio/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/GeiserX/ghost-github-portfolio/ci.yml?style=flat-square&logo=github&label=CI" alt="CI"></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/GeiserX/ghost-github-portfolio?style=flat-square" alt="License"></a>
|
|
10
|
+
<a href="https://github.com/GeiserX/ghost-github-portfolio/stargazers"><img src="https://img.shields.io/github/stars/GeiserX/ghost-github-portfolio?style=flat-square&logo=github" alt="Stars"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><strong>Auto-sync your GitHub repositories to a Ghost CMS portfolio page.</strong></p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What it does
|
|
18
|
+
|
|
19
|
+
`ghost-github-portfolio` fetches your public GitHub repositories, sorts them by stars, generates beautiful portfolio cards with banners and badges, and pushes the result to a Ghost page via the Admin API.
|
|
20
|
+
|
|
21
|
+
Run it daily via cron, GitHub Actions, or Docker to keep your portfolio always up to date.
|
|
22
|
+
|
|
23
|
+
**Features:**
|
|
24
|
+
|
|
25
|
+
- Fetches repos sorted by stars, filtered by configurable minimum
|
|
26
|
+
- Auto-detects banner images (`docs/images/banner.svg`, `media/banner.svg`, etc.)
|
|
27
|
+
- Dynamic shields.io badges (stars, forks, license, Docker pulls, website, awesome-list)
|
|
28
|
+
- Per-repo overrides (custom description, badges, Docker image, tech stack)
|
|
29
|
+
- Ghost lexical editor format (the current Ghost editor)
|
|
30
|
+
- Dry-run mode for previewing changes
|
|
31
|
+
- Runs as CLI, Docker container, or GitHub Action
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
### 1. Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# npm (global)
|
|
39
|
+
npm install -g ghost-github-portfolio
|
|
40
|
+
|
|
41
|
+
# npx (no install)
|
|
42
|
+
npx ghost-github-portfolio
|
|
43
|
+
|
|
44
|
+
# Docker
|
|
45
|
+
docker pull drumsergio/ghost-github-portfolio:0.2.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Create a config
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
ghost-github-portfolio init
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This generates a `config.yml` with all available options. Edit it with your GitHub username and Ghost API credentials.
|
|
55
|
+
|
|
56
|
+
### 3. Get your Ghost Admin API key
|
|
57
|
+
|
|
58
|
+
1. Go to your Ghost Admin panel > **Settings** > **Integrations**
|
|
59
|
+
2. Create a new **Custom Integration**
|
|
60
|
+
3. Copy the **Admin API Key** (format: `KEY_ID:SECRET`)
|
|
61
|
+
|
|
62
|
+
### 4. Run
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Preview (no changes to Ghost)
|
|
66
|
+
ghost-github-portfolio sync --config config.yml --dry-run --verbose
|
|
67
|
+
|
|
68
|
+
# Sync to Ghost
|
|
69
|
+
ghost-github-portfolio sync --config config.yml --verbose
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
github:
|
|
76
|
+
username: YOUR_GITHUB_USERNAME
|
|
77
|
+
# token: ghp_xxx # Optional: higher rate limits (env: GHOST_GITHUB_TOKEN)
|
|
78
|
+
|
|
79
|
+
ghost:
|
|
80
|
+
url: https://your-ghost-blog.com
|
|
81
|
+
adminApiKey: "KEY_ID:SECRET_HEX"
|
|
82
|
+
pageSlug: portfolio # or pageId: "hex_id"
|
|
83
|
+
|
|
84
|
+
portfolio:
|
|
85
|
+
minStars: 2
|
|
86
|
+
maxRepos: 50
|
|
87
|
+
includeForked: false
|
|
88
|
+
badgeStyle: for-the-badge # flat, flat-square, for-the-badge, plastic, social
|
|
89
|
+
showBanner: true
|
|
90
|
+
centerContent: true
|
|
91
|
+
defaultBannerPath: docs/images/banner.svg
|
|
92
|
+
|
|
93
|
+
bannerPaths:
|
|
94
|
+
awesome-spain: media/banner.svg # Override per repo
|
|
95
|
+
|
|
96
|
+
excludeRepos:
|
|
97
|
+
- .github
|
|
98
|
+
|
|
99
|
+
repos:
|
|
100
|
+
my-project:
|
|
101
|
+
description: "Custom description"
|
|
102
|
+
dockerImage: myuser/my-project # Adds Docker pulls badge
|
|
103
|
+
techStack: "Python, Docker, Redis"
|
|
104
|
+
badges:
|
|
105
|
+
- type: website
|
|
106
|
+
url: https://my-project.com
|
|
107
|
+
- type: awesome-list
|
|
108
|
+
- type: platform
|
|
109
|
+
label: macOS
|
|
110
|
+
logo: apple
|
|
111
|
+
- type: docs
|
|
112
|
+
url: https://docs.my-project.com
|
|
113
|
+
- type: custom
|
|
114
|
+
label: MCP
|
|
115
|
+
value: Official Registry
|
|
116
|
+
color: E6522C
|
|
117
|
+
|
|
118
|
+
footer:
|
|
119
|
+
showStats: true
|
|
120
|
+
showViewAll: true
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Environment variables
|
|
124
|
+
|
|
125
|
+
| Variable | Description |
|
|
126
|
+
|----------|-------------|
|
|
127
|
+
| `GHOST_GITHUB_TOKEN` | GitHub token for private repos / higher rate limits |
|
|
128
|
+
| `GHOST_ADMIN_API_KEY` | Ghost Admin API key (overrides config file) |
|
|
129
|
+
|
|
130
|
+
## Docker
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
docker run --rm \
|
|
134
|
+
-v /path/to/config.yml:/config/config.yml \
|
|
135
|
+
drumsergio/ghost-github-portfolio:0.2.0
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Docker Compose (daily cron)
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
services:
|
|
142
|
+
ghost-portfolio:
|
|
143
|
+
image: drumsergio/ghost-github-portfolio:0.2.0
|
|
144
|
+
volumes:
|
|
145
|
+
- ./config.yml:/config/config.yml:ro
|
|
146
|
+
# Run daily at 6 AM via external cron or restart policy
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## GitHub Action
|
|
150
|
+
|
|
151
|
+
Create `.github/workflows/portfolio.yml` in any repo:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
name: Update Portfolio
|
|
155
|
+
|
|
156
|
+
on:
|
|
157
|
+
schedule:
|
|
158
|
+
- cron: "0 6 * * *" # Daily at 6 AM UTC
|
|
159
|
+
workflow_dispatch: # Manual trigger
|
|
160
|
+
|
|
161
|
+
jobs:
|
|
162
|
+
sync:
|
|
163
|
+
runs-on: ubuntu-latest
|
|
164
|
+
steps:
|
|
165
|
+
- uses: actions/checkout@v4
|
|
166
|
+
|
|
167
|
+
- uses: actions/setup-node@v4
|
|
168
|
+
with:
|
|
169
|
+
node-version: 22
|
|
170
|
+
|
|
171
|
+
- run: npx ghost-github-portfolio sync --config config.yml --verbose
|
|
172
|
+
env:
|
|
173
|
+
GHOST_GITHUB_TOKEN: ${{ secrets.GHOST_GITHUB_TOKEN }}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Store `config.yml` in the repo (without secrets) and use environment variables for the API keys.
|
|
177
|
+
|
|
178
|
+
## How it works
|
|
179
|
+
|
|
180
|
+
1. **Fetches** all public repos for the configured GitHub user via the REST API
|
|
181
|
+
2. **Filters** by minimum stars, excludes forks and blocklisted repos
|
|
182
|
+
3. **Sorts** by star count (descending)
|
|
183
|
+
4. **Detects** banner images by checking common paths (`docs/images/banner.svg`, `media/banner.svg`, etc.)
|
|
184
|
+
5. **Generates** HTML cards with: banner, name, dynamic badges, description, tech stack
|
|
185
|
+
6. **Composes** a Ghost lexical JSON document (the format Ghost's editor uses internally)
|
|
186
|
+
7. **Updates** the target Ghost page via the Admin API with JWT authentication
|
|
187
|
+
|
|
188
|
+
Badges are dynamic (served by shields.io) — stars, forks, and Docker pulls update automatically on every page view without re-running the tool.
|
|
189
|
+
|
|
190
|
+
## Badge types
|
|
191
|
+
|
|
192
|
+
| Type | Auto-detected | Description |
|
|
193
|
+
|------|--------------|-------------|
|
|
194
|
+
| Stars | Yes | Always shown |
|
|
195
|
+
| Forks | Yes | Always shown |
|
|
196
|
+
| License | Yes | From GitHub repo metadata |
|
|
197
|
+
| Docker pulls | Config | Set `dockerImage` in repo overrides |
|
|
198
|
+
| Website | Yes | From GitHub homepage field |
|
|
199
|
+
| Awesome list | Yes | If repo has `awesome-list` topic |
|
|
200
|
+
| Docs | Yes | If homepage is a GitHub Pages URL |
|
|
201
|
+
| Platform | Config | Custom platform badge (macOS, Linux, etc.) |
|
|
202
|
+
| Custom | Config | Any shields.io-compatible badge |
|
|
203
|
+
|
|
204
|
+
## Roadmap
|
|
205
|
+
|
|
206
|
+
See [docs/ROADMAP.md](docs/ROADMAP.md) for the full roadmap — themes, multi-CMS support, AI features, analytics, team portfolios, and more.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
[GPL-3.0](LICENSE)
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAmB,MAAM,YAAY,CAAC;AAgB1D,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkD/C;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAsD9C"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
const DEFAULT_PORTFOLIO = {
|
|
4
|
+
minStars: 2,
|
|
5
|
+
maxRepos: 50,
|
|
6
|
+
excludeRepos: [],
|
|
7
|
+
includeForked: false,
|
|
8
|
+
badgeStyle: "for-the-badge",
|
|
9
|
+
showBanner: true,
|
|
10
|
+
centerContent: true,
|
|
11
|
+
defaultBannerPath: "docs/images/banner.svg",
|
|
12
|
+
bannerPaths: {},
|
|
13
|
+
repos: {},
|
|
14
|
+
footer: { showStats: true, showViewAll: true },
|
|
15
|
+
};
|
|
16
|
+
export function loadConfig(path) {
|
|
17
|
+
const raw = readFileSync(path, "utf-8");
|
|
18
|
+
const parsed = parse(raw);
|
|
19
|
+
if (!parsed.github || !parsed.github.username) {
|
|
20
|
+
throw new Error("Config: github.username is required");
|
|
21
|
+
}
|
|
22
|
+
if (!parsed.ghost || !parsed.ghost.url) {
|
|
23
|
+
throw new Error("Config: ghost.url is required");
|
|
24
|
+
}
|
|
25
|
+
if (!parsed.ghost.adminApiKey) {
|
|
26
|
+
throw new Error("Config: ghost.adminApiKey is required");
|
|
27
|
+
}
|
|
28
|
+
const ghost = parsed.ghost;
|
|
29
|
+
if (!ghost.pageId && !ghost.pageSlug) {
|
|
30
|
+
throw new Error("Config: ghost.pageId or ghost.pageSlug is required");
|
|
31
|
+
}
|
|
32
|
+
const github = parsed.github;
|
|
33
|
+
const portfolio = (parsed.portfolio ?? {});
|
|
34
|
+
// Env var overrides (GHOST_GITHUB_TOKEN only — GITHUB_TOKEN is not used to
|
|
35
|
+
// avoid picking up unrelated tokens from gh CLI or CI environments)
|
|
36
|
+
const ghToken = github.token ?? process.env.GHOST_GITHUB_TOKEN;
|
|
37
|
+
const ghostKey = ghost.adminApiKey ?? process.env.GHOST_ADMIN_API_KEY;
|
|
38
|
+
return {
|
|
39
|
+
github: {
|
|
40
|
+
username: github.username,
|
|
41
|
+
token: ghToken,
|
|
42
|
+
},
|
|
43
|
+
ghost: {
|
|
44
|
+
url: ghost.url.replace(/\/$/, ""),
|
|
45
|
+
adminApiKey: ghostKey,
|
|
46
|
+
pageId: ghost.pageId,
|
|
47
|
+
pageSlug: ghost.pageSlug,
|
|
48
|
+
},
|
|
49
|
+
portfolio: {
|
|
50
|
+
...DEFAULT_PORTFOLIO,
|
|
51
|
+
...portfolio,
|
|
52
|
+
footer: { ...DEFAULT_PORTFOLIO.footer, ...portfolio.footer },
|
|
53
|
+
bannerPaths: {
|
|
54
|
+
...DEFAULT_PORTFOLIO.bannerPaths,
|
|
55
|
+
...portfolio.bannerPaths,
|
|
56
|
+
},
|
|
57
|
+
repos: { ...DEFAULT_PORTFOLIO.repos, ...portfolio.repos },
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function generateExampleConfig() {
|
|
62
|
+
return `# ghost-github-portfolio configuration
|
|
63
|
+
github:
|
|
64
|
+
username: YOUR_GITHUB_USERNAME
|
|
65
|
+
# token: ghp_xxx # Optional: for private repos or higher rate limits (env: GHOST_GITHUB_TOKEN)
|
|
66
|
+
|
|
67
|
+
ghost:
|
|
68
|
+
url: https://your-ghost-blog.com
|
|
69
|
+
adminApiKey: "KEY_ID:SECRET_HEX" # From Ghost Admin > Integrations (env: GHOST_ADMIN_API_KEY)
|
|
70
|
+
pageSlug: portfolio # Update this page (or use pageId for the hex ID)
|
|
71
|
+
|
|
72
|
+
portfolio:
|
|
73
|
+
minStars: 2 # Only show repos with at least this many stars
|
|
74
|
+
maxRepos: 50 # Maximum number of repos to display
|
|
75
|
+
includeForked: false # Skip forked repos
|
|
76
|
+
badgeStyle: for-the-badge # shields.io style: flat, flat-square, for-the-badge, plastic, social
|
|
77
|
+
showBanner: true # Show repo banner images (SVG/PNG from the repo)
|
|
78
|
+
centerContent: true # Center project names and badges
|
|
79
|
+
|
|
80
|
+
defaultBannerPath: docs/images/banner.svg # Default location to look for banners
|
|
81
|
+
|
|
82
|
+
# Override banner path for specific repos
|
|
83
|
+
bannerPaths:
|
|
84
|
+
# awesome-spain: media/banner.svg
|
|
85
|
+
|
|
86
|
+
excludeRepos:
|
|
87
|
+
- .github # Profile config repo
|
|
88
|
+
# - my-private-project
|
|
89
|
+
|
|
90
|
+
# Per-repo overrides
|
|
91
|
+
repos: {}
|
|
92
|
+
# my-project:
|
|
93
|
+
# description: "Custom description (overrides GitHub)"
|
|
94
|
+
# dockerImage: drumsergio/my-project # Adds Docker pulls badge
|
|
95
|
+
# techStack: "Python, Docker, Redis"
|
|
96
|
+
# bannerPath: assets/banner.png
|
|
97
|
+
# badges:
|
|
98
|
+
# - type: website
|
|
99
|
+
# url: https://my-project.com
|
|
100
|
+
# - type: awesome-list
|
|
101
|
+
# - type: platform
|
|
102
|
+
# label: macOS
|
|
103
|
+
# logo: apple
|
|
104
|
+
# - type: docs
|
|
105
|
+
# url: https://docs.my-project.com
|
|
106
|
+
# - type: custom
|
|
107
|
+
# label: MCP
|
|
108
|
+
# value: Official Registry
|
|
109
|
+
# color: E6522C
|
|
110
|
+
|
|
111
|
+
footer:
|
|
112
|
+
showStats: true # Show total stars and repo count
|
|
113
|
+
showViewAll: true # Show "View All Repositories" link
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAG7B,MAAM,iBAAiB,GAAoB;IACzC,QAAQ,EAAE,CAAC;IACX,QAAQ,EAAE,EAAE;IACZ,YAAY,EAAE,EAAE;IAChB,aAAa,EAAE,KAAK;IACpB,UAAU,EAAE,eAAe;IAC3B,UAAU,EAAE,IAAI;IAChB,aAAa,EAAE,IAAI;IACnB,iBAAiB,EAAE,wBAAwB;IAC3C,WAAW,EAAE,EAAE;IACf,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE;CAC/C,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACxC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAA4B,CAAC;IAErD,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAE,MAAM,CAAC,MAAkC,CAAC,QAAQ,EAAE,CAAC;QAC3E,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,CAAE,MAAM,CAAC,KAAiC,CAAC,GAAG,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IACD,IAAI,CAAE,MAAM,CAAC,KAAiC,CAAC,WAAW,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,KAAgC,CAAC;IACtD,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAgC,CAAC;IACvD,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAA6B,CAAC;IAEvE,2EAA2E;IAC3E,oEAAoE;IACpE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC/D,MAAM,QAAQ,GACX,KAAK,CAAC,WAAsB,IAAI,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC;IAEnE,OAAO;QACL,MAAM,EAAE;YACN,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,OAAO;SACf;QACD,KAAK,EAAE;YACL,GAAG,EAAG,KAAK,CAAC,GAAc,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;YAC7C,WAAW,EAAE,QAAQ;YACrB,MAAM,EAAE,KAAK,CAAC,MAA4B;YAC1C,QAAQ,EAAE,KAAK,CAAC,QAA8B;SAC/C;QACD,SAAS,EAAE;YACT,GAAG,iBAAiB;YACpB,GAAG,SAAS;YACZ,MAAM,EAAE,EAAE,GAAG,iBAAiB,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,EAAE;YAC5D,WAAW,EAAE;gBACX,GAAG,iBAAiB,CAAC,WAAW;gBAChC,GAAG,SAAS,CAAC,WAAW;aACzB;YACD,KAAK,EAAE,EAAE,GAAG,iBAAiB,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE;SAC1D;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDR,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Config, GitHubRepo, LexicalDocument } from "./types.js";
|
|
2
|
+
export declare function generateCard(repo: GitHubRepo, bannerUrl: string | null, config: Config): string;
|
|
3
|
+
export declare function generateFooter(repos: GitHubRepo[], config: Config): string | null;
|
|
4
|
+
export declare function buildLexical(cards: string[], footer: string | null): LexicalDocument;
|
|
5
|
+
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,MAAM,EACN,UAAU,EACV,eAAe,EAEhB,MAAM,YAAY,CAAC;AAyGpB,wBAAgB,YAAY,CAC1B,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,EAAE,MAAM,GACb,MAAM,CA0DR;AAuCD,wBAAgB,cAAc,CAC5B,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,MAAM,GAAG,IAAI,CAoDf;AAED,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,eAAe,CA0BjB"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
function shieldsUrl(label, value, color, style, logo) {
|
|
2
|
+
const encoded = `${encodeURIComponent(label)}-${encodeURIComponent(value)}-${color}`;
|
|
3
|
+
let url = `https://img.shields.io/badge/${encoded}?style=${style}`;
|
|
4
|
+
if (logo)
|
|
5
|
+
url += `&logo=${logo}&logoColor=white`;
|
|
6
|
+
return url;
|
|
7
|
+
}
|
|
8
|
+
function buildBadges(repo, config) {
|
|
9
|
+
const style = config.portfolio.badgeStyle;
|
|
10
|
+
const overrides = config.portfolio.repos[repo.name];
|
|
11
|
+
const badges = [];
|
|
12
|
+
// Stars
|
|
13
|
+
badges.push(`<img src="https://img.shields.io/github/stars/${repo.full_name}?style=${style}" alt="Stars">`);
|
|
14
|
+
// Forks
|
|
15
|
+
badges.push(`<img src="https://img.shields.io/github/forks/${repo.full_name}?style=${style}" alt="Forks">`);
|
|
16
|
+
// License
|
|
17
|
+
if (repo.license) {
|
|
18
|
+
badges.push(`<img src="https://img.shields.io/github/license/${repo.full_name}?style=${style}" alt="License">`);
|
|
19
|
+
}
|
|
20
|
+
// Docker pulls (from config override)
|
|
21
|
+
const dockerImage = overrides?.dockerImage;
|
|
22
|
+
if (dockerImage) {
|
|
23
|
+
badges.push(`<a href="https://hub.docker.com/r/${dockerImage}"><img src="https://img.shields.io/docker/pulls/${dockerImage}?style=${style}" alt="Docker Pulls"></a>`);
|
|
24
|
+
}
|
|
25
|
+
// Website badge (from GitHub homepage or config)
|
|
26
|
+
const homepage = repo.homepage;
|
|
27
|
+
if (homepage &&
|
|
28
|
+
!homepage.includes("hub.docker.com") &&
|
|
29
|
+
!homepage.includes("github.io")) {
|
|
30
|
+
const domain = new URL(homepage).hostname;
|
|
31
|
+
badges.push(`<a href="${homepage}"><img src="${shieldsUrl("website", domain, "blue", style)}" alt="Website"></a>`);
|
|
32
|
+
}
|
|
33
|
+
// Custom badges from config
|
|
34
|
+
if (overrides?.badges) {
|
|
35
|
+
for (const badge of overrides.badges) {
|
|
36
|
+
badges.push(renderCustomBadge(badge, style));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Auto-detect awesome-list from topics
|
|
40
|
+
if (repo.topics.includes("awesome-list") &&
|
|
41
|
+
!overrides?.badges?.some((b) => b.type === "awesome-list")) {
|
|
42
|
+
badges.push(`<img src="${shieldsUrl("awesome", "list", "fc60a8", style, "awesomelists")}" alt="Awesome List">`);
|
|
43
|
+
}
|
|
44
|
+
// Docs badge for GitHub Pages
|
|
45
|
+
if (homepage?.includes("github.io") &&
|
|
46
|
+
!overrides?.badges?.some((b) => b.type === "docs")) {
|
|
47
|
+
badges.push(`<a href="${homepage}"><img src="${shieldsUrl("docs", "online", "blue", style)}" alt="Docs"></a>`);
|
|
48
|
+
}
|
|
49
|
+
return badges;
|
|
50
|
+
}
|
|
51
|
+
function renderCustomBadge(badge, style) {
|
|
52
|
+
switch (badge.type) {
|
|
53
|
+
case "website":
|
|
54
|
+
return `<a href="${badge.url}"><img src="${shieldsUrl("website", badge.label ?? new URL(badge.url).hostname, badge.color ?? "blue", style)}" alt="Website"></a>`;
|
|
55
|
+
case "docker":
|
|
56
|
+
return `<a href="https://hub.docker.com/r/${badge.label}"><img src="https://img.shields.io/docker/pulls/${badge.label}?style=${style}" alt="Docker Pulls"></a>`;
|
|
57
|
+
case "awesome-list":
|
|
58
|
+
return `<img src="${shieldsUrl("awesome", "list", badge.color ?? "fc60a8", style, "awesomelists")}" alt="Awesome List">`;
|
|
59
|
+
case "platform":
|
|
60
|
+
return `<img src="${shieldsUrl("platform", badge.label ?? "unknown", badge.color ?? "000", style, badge.logo)}" alt="Platform">`;
|
|
61
|
+
case "docs":
|
|
62
|
+
return `<a href="${badge.url}"><img src="${shieldsUrl("docs", badge.label ?? "online", badge.color ?? "blue", style)}" alt="Docs"></a>`;
|
|
63
|
+
case "custom":
|
|
64
|
+
return `<img src="${shieldsUrl(badge.label ?? "", badge.value ?? "", badge.color ?? "grey", style, badge.logo)}" alt="${badge.label}">`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function generateCard(repo, bannerUrl, config) {
|
|
68
|
+
const overrides = config.portfolio.repos[repo.name];
|
|
69
|
+
const center = config.portfolio.centerContent;
|
|
70
|
+
const align = center ? ' style="text-align:center;"' : "";
|
|
71
|
+
const flexJustify = center ? " justify-content:center;" : "";
|
|
72
|
+
const parts = [];
|
|
73
|
+
// Banner
|
|
74
|
+
if (bannerUrl) {
|
|
75
|
+
parts.push(`<a href="${repo.html_url}" rel="noreferrer"${center ? ' style="display:block; text-align:center;"' : ""}>` +
|
|
76
|
+
`<img src="${bannerUrl}" alt="${repo.name} banner" style="width:100%; max-width:900px; border-radius:8px; margin-bottom:12px; display:block;${center ? " margin-left:auto; margin-right:auto;" : ""}">` +
|
|
77
|
+
`</a>`);
|
|
78
|
+
}
|
|
79
|
+
// Title
|
|
80
|
+
parts.push(`<h3${align}><a href="${repo.html_url}" rel="noreferrer">${repo.name}</a></h3>`);
|
|
81
|
+
// Badges
|
|
82
|
+
const badges = buildBadges(repo, config);
|
|
83
|
+
parts.push(`<div style="display:flex; gap:4px; align-items:center; flex-wrap:wrap;${flexJustify}">`, ` ${badges.join("\n ")}`, `</div>`);
|
|
84
|
+
// Description
|
|
85
|
+
const description = overrides?.description ?? repo.description;
|
|
86
|
+
if (description) {
|
|
87
|
+
parts.push(`<p><em>${escapeHtml(description)}</em></p>`);
|
|
88
|
+
}
|
|
89
|
+
// Key features (from config only)
|
|
90
|
+
if (overrides?.keyFeatures?.length) {
|
|
91
|
+
parts.push(`<p><strong>Key Features:</strong></p>`);
|
|
92
|
+
parts.push(`<ul>`);
|
|
93
|
+
for (const feature of overrides.keyFeatures) {
|
|
94
|
+
parts.push(`<li>${escapeHtml(feature)}</li>`);
|
|
95
|
+
}
|
|
96
|
+
parts.push(`</ul>`);
|
|
97
|
+
}
|
|
98
|
+
// Tech stack
|
|
99
|
+
const techStack = overrides?.techStack ?? inferTechStack(repo);
|
|
100
|
+
if (techStack) {
|
|
101
|
+
parts.push(`<p><strong>Tech Stack:</strong> ${escapeHtml(techStack)}</p>`);
|
|
102
|
+
}
|
|
103
|
+
// Separator
|
|
104
|
+
parts.push(`<hr>`);
|
|
105
|
+
return parts.join("\n");
|
|
106
|
+
}
|
|
107
|
+
function inferTechStack(repo) {
|
|
108
|
+
const languages = [];
|
|
109
|
+
if (repo.language)
|
|
110
|
+
languages.push(repo.language);
|
|
111
|
+
// Add relevant topics as tech
|
|
112
|
+
const techTopics = repo.topics.filter((t) => [
|
|
113
|
+
"docker",
|
|
114
|
+
"kubernetes",
|
|
115
|
+
"helm",
|
|
116
|
+
"nextjs",
|
|
117
|
+
"react",
|
|
118
|
+
"vue",
|
|
119
|
+
"svelte",
|
|
120
|
+
"fastapi",
|
|
121
|
+
"flask",
|
|
122
|
+
"express",
|
|
123
|
+
"nestjs",
|
|
124
|
+
"terraform",
|
|
125
|
+
"ansible",
|
|
126
|
+
"mcp",
|
|
127
|
+
"jellyfin",
|
|
128
|
+
"telegram",
|
|
129
|
+
"swift",
|
|
130
|
+
"macos",
|
|
131
|
+
"nodejs",
|
|
132
|
+
].includes(t.toLowerCase()));
|
|
133
|
+
const topicLabels = techTopics.map((t) => t.charAt(0).toUpperCase() + t.slice(1));
|
|
134
|
+
const all = [...new Set([...languages, ...topicLabels])];
|
|
135
|
+
return all.length > 0 ? all.join(", ") : null;
|
|
136
|
+
}
|
|
137
|
+
export function generateFooter(repos, config) {
|
|
138
|
+
if (!config.portfolio.footer.showStats && !config.portfolio.footer.showViewAll)
|
|
139
|
+
return null;
|
|
140
|
+
const center = config.portfolio.centerContent;
|
|
141
|
+
const align = center ? ' style="text-align:center;"' : "";
|
|
142
|
+
const listStyle = center
|
|
143
|
+
? ' style="list-style:none; padding:0; text-align:center;"'
|
|
144
|
+
: "";
|
|
145
|
+
const parts = [];
|
|
146
|
+
parts.push(`<h2${align}>GitHub Stats</h2>`);
|
|
147
|
+
if (config.portfolio.footer.showStats) {
|
|
148
|
+
const totalStars = repos.reduce((s, r) => s + r.stargazers_count, 0);
|
|
149
|
+
const totalRepos = repos.length;
|
|
150
|
+
const languages = [
|
|
151
|
+
...new Set(repos.map((r) => r.language).filter(Boolean)),
|
|
152
|
+
];
|
|
153
|
+
const focusAreas = [
|
|
154
|
+
...new Set(repos.flatMap((r) => r.topics).filter(Boolean)),
|
|
155
|
+
]
|
|
156
|
+
.slice(0, 8)
|
|
157
|
+
.map((t) => t.charAt(0).toUpperCase() + t.slice(1));
|
|
158
|
+
parts.push(`<ul${listStyle}>`);
|
|
159
|
+
parts.push(`<li><strong>Total Stars (featured):</strong> ${totalStars}+</li>`);
|
|
160
|
+
parts.push(`<li><strong>Featured Repositories:</strong> ${totalRepos}</li>`);
|
|
161
|
+
if (languages.length > 0) {
|
|
162
|
+
parts.push(`<li><strong>Primary Languages:</strong> ${languages.join(", ")}</li>`);
|
|
163
|
+
}
|
|
164
|
+
if (focusAreas.length > 0) {
|
|
165
|
+
parts.push(`<li><strong>Focus Areas:</strong> ${focusAreas.join(", ")}</li>`);
|
|
166
|
+
}
|
|
167
|
+
parts.push(`</ul>`);
|
|
168
|
+
}
|
|
169
|
+
if (config.portfolio.footer.showViewAll) {
|
|
170
|
+
parts.push(`<p${align}><a href="https://github.com/${config.github.username}?tab=repositories"><strong>View All Repositories →</strong></a></p>`);
|
|
171
|
+
}
|
|
172
|
+
return parts.join("\n");
|
|
173
|
+
}
|
|
174
|
+
export function buildLexical(cards, footer) {
|
|
175
|
+
const children = [];
|
|
176
|
+
// Opening separator
|
|
177
|
+
children.push({ type: "horizontalrule", version: 1 });
|
|
178
|
+
// Project cards
|
|
179
|
+
for (const card of cards) {
|
|
180
|
+
children.push({ type: "html", version: 1, html: card });
|
|
181
|
+
}
|
|
182
|
+
// Footer
|
|
183
|
+
if (footer) {
|
|
184
|
+
children.push({ type: "html", version: 1, html: footer });
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
root: {
|
|
188
|
+
children,
|
|
189
|
+
direction: "ltr",
|
|
190
|
+
format: "",
|
|
191
|
+
indent: 0,
|
|
192
|
+
type: "root",
|
|
193
|
+
version: 1,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function escapeHtml(str) {
|
|
198
|
+
return str
|
|
199
|
+
.replace(/&/g, "&")
|
|
200
|
+
.replace(/</g, "<")
|
|
201
|
+
.replace(/>/g, ">")
|
|
202
|
+
.replace(/"/g, """);
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
|
package/dist/ghost.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Config, LexicalDocument } from "./types.js";
|
|
2
|
+
export declare function generateJwt(apiKey: string): string;
|
|
3
|
+
interface GhostPage {
|
|
4
|
+
id: string;
|
|
5
|
+
updated_at: string;
|
|
6
|
+
title: string;
|
|
7
|
+
lexical: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function fetchPage(config: Config): Promise<GhostPage>;
|
|
10
|
+
export declare function updatePage(config: Config, pageId: string, updatedAt: string, lexical: LexicalDocument): Promise<GhostPage>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=ghost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.d.ts","sourceRoot":"","sources":["../src/ghost.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAG1D,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CA6BlD;AAUD,UAAU,SAAS;IACjB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAqBlE;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,SAAS,CAAC,CA0BpB"}
|
package/dist/ghost.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { fetchWithRetry } from "./http.js";
|
|
3
|
+
export function generateJwt(apiKey) {
|
|
4
|
+
const [keyId, secretHex] = apiKey.split(":");
|
|
5
|
+
if (!keyId || !secretHex) {
|
|
6
|
+
throw new Error('Invalid Ghost Admin API key format. Expected "KEY_ID:SECRET_HEX"');
|
|
7
|
+
}
|
|
8
|
+
const secret = Buffer.from(secretHex, "hex");
|
|
9
|
+
const now = Math.floor(Date.now() / 1000);
|
|
10
|
+
const header = Buffer.from(JSON.stringify({ alg: "HS256", kid: keyId, typ: "JWT" }))
|
|
11
|
+
.toString("base64url")
|
|
12
|
+
.replace(/=+$/, "");
|
|
13
|
+
const payload = Buffer.from(JSON.stringify({ iat: now, exp: now + 300, aud: "/admin/" }))
|
|
14
|
+
.toString("base64url")
|
|
15
|
+
.replace(/=+$/, "");
|
|
16
|
+
const signature = createHmac("sha256", secret)
|
|
17
|
+
.update(`${header}.${payload}`)
|
|
18
|
+
.digest("base64url")
|
|
19
|
+
.replace(/=+$/, "");
|
|
20
|
+
return `${header}.${payload}.${signature}`;
|
|
21
|
+
}
|
|
22
|
+
function ghostHeaders(apiKey) {
|
|
23
|
+
return {
|
|
24
|
+
Authorization: `Ghost ${generateJwt(apiKey)}`,
|
|
25
|
+
"Content-Type": "application/json",
|
|
26
|
+
"User-Agent": "Mozilla/5.0 ghost-github-portfolio",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export async function fetchPage(config) {
|
|
30
|
+
const { url, adminApiKey, pageId, pageSlug } = config.ghost;
|
|
31
|
+
let endpoint;
|
|
32
|
+
if (pageId) {
|
|
33
|
+
endpoint = `${url}/ghost/api/admin/pages/${pageId}/`;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
endpoint = `${url}/ghost/api/admin/pages/slug/${pageSlug}/`;
|
|
37
|
+
}
|
|
38
|
+
const res = await fetchWithRetry(endpoint, {
|
|
39
|
+
headers: ghostHeaders(adminApiKey),
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const body = await res.text();
|
|
43
|
+
throw new Error(`Ghost API error ${res.status}: ${body}`);
|
|
44
|
+
}
|
|
45
|
+
const data = (await res.json());
|
|
46
|
+
return data.pages[0];
|
|
47
|
+
}
|
|
48
|
+
export async function updatePage(config, pageId, updatedAt, lexical) {
|
|
49
|
+
const { url, adminApiKey } = config.ghost;
|
|
50
|
+
const endpoint = `${url}/ghost/api/admin/pages/${pageId}/`;
|
|
51
|
+
const body = {
|
|
52
|
+
pages: [
|
|
53
|
+
{
|
|
54
|
+
lexical: JSON.stringify(lexical),
|
|
55
|
+
updated_at: updatedAt,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
const res = await fetchWithRetry(endpoint, {
|
|
60
|
+
method: "PUT",
|
|
61
|
+
headers: ghostHeaders(adminApiKey),
|
|
62
|
+
body: JSON.stringify(body),
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
throw new Error(`Ghost update error ${res.status}: ${text}`);
|
|
67
|
+
}
|
|
68
|
+
const data = (await res.json());
|
|
69
|
+
return data.pages[0];
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=ghost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ghost.js","sourceRoot":"","sources":["../src/ghost.ts"],"names":[],"mappings":"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"}
|