Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions .github/scripts/generate-stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const USERNAME = 'Codestz';
const PURPLE = '#7c3aed';
const TOKEN = process.env.GITHUB_TOKEN;

// Featured repos rendered as standalone cards (stars/forks auto-refreshed)
const FEATURED_REPOS = ['claude-hindsight'];

if (!TOKEN) {
console.error('GITHUB_TOKEN is required');
process.exit(1);
Expand Down Expand Up @@ -69,6 +72,19 @@ async function fetchStats() {
return data.user;
}

async function fetchRepo(name) {
const { data } = await githubAPI(`{
repository(owner: "${USERNAME}", name: "${name}") {
name
description
stargazerCount
forkCount
primaryLanguage { name color }
}
}`);
return data.repository;
}

function calculateStreak(weeks) {
const days = weeks
.flatMap((w) => w.contributionDays)
Expand Down Expand Up @@ -340,6 +356,73 @@ function generateContributionGraphSVG(weeks, theme) {
</svg>`;
}

function escapeXml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

function wrapText(text, maxChars, maxLines) {
const words = String(text || '')
.split(/\s+/)
.filter(Boolean);
const all = [];
let line = '';
for (const word of words) {
const candidate = line ? `${line} ${word}` : word;
if (candidate.length > maxChars && line) {
all.push(line);
line = word;
} else {
line = candidate;
}
}
if (line) all.push(line);
if (all.length <= maxLines) return all;

const kept = all.slice(0, maxLines);
const last = kept[maxLines - 1];
kept[maxLines - 1] = (last.length > maxChars - 1 ? last.slice(0, maxChars - 1) : last) + '…';
return kept;
}

function generateRepoCardSVG(repo, theme) {
const isDark = theme === 'dark';
const textColor = isDark ? '#c9d1d9' : '#333333';
const subColor = isDark ? '#888888' : '#666666';
const lang = repo.primaryLanguage;
const descLines = wrapText(repo.description, 52, 2);

const descSvg = descLines
.map(
(l, i) =>
`<text x="25" y="${66 + i * 20}" fill="${subColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${escapeXml(l)}</text>`
)
.join('\n ');

const footerY = 66 + descLines.length * 20 + 18;

const langSvg = lang
? `<circle cx="29" cy="${footerY - 4}" r="6" fill="${lang.color || PURPLE}"/>
<text x="42" y="${footerY}" fill="${textColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${escapeXml(lang.name)}</text>`
: '';

const height = footerY + 20;

return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="${height}" viewBox="0 0 400 ${height}">
<rect x="2" y="2" width="396" height="${height - 4}" fill="none" stroke="${PURPLE}" stroke-width="3"/>
<svg x="21" y="20" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="${PURPLE}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>
</svg>
<text x="48" y="34" fill="${PURPLE}" font-size="17" font-weight="bold" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${escapeXml(repo.name)}</text>
${descSvg}
${langSvg}
<text x="300" y="${footerY}" fill="${textColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">β˜… ${repo.stargazerCount.toLocaleString()}</text>
<text x="350" y="${footerY}" fill="${textColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">β‘‚ ${repo.forkCount.toLocaleString()}</text>
</svg>`;
}

async function main() {
console.log('Fetching GitHub stats...');
const stats = await fetchStats();
Expand Down Expand Up @@ -374,6 +457,25 @@ async function main() {
console.log(`Generated ${theme} theme SVGs`);
}

for (const name of FEATURED_REPOS) {
try {
const repo = await fetchRepo(name);
if (!repo) {
console.log(`Repo ${name} not found, skipping card`);
continue;
}
for (const theme of ['dark', 'light']) {
fs.writeFileSync(
path.join(outDir, `repo-${name}-${theme}.svg`),
generateRepoCardSVG(repo, theme)
);
}
console.log(`Generated repo card for ${name}`);
} catch (err) {
console.log(`Failed to generate card for ${name}: ${err.message}`);
}
}

console.log('Done! SVGs written to public/stats/');
}

Expand Down
25 changes: 20 additions & 5 deletions .github/workflows/update-readme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,32 @@ jobs:

try {
const rss = await fetchRSS('https://codestz.dev/feed');
const items = [...rss.matchAll(/<item>[\s\S]*?<title>(.*?)<\/title>[\s\S]*?<link>(.*?)<\/link>[\s\S]*?<\/item>/g)];
const items = [...rss.matchAll(/<item>([\s\S]*?)<\/item>/g)];

if (items.length === 0) {
console.log('No items found in RSS feed, skipping update');
return;
}

const posts = items.slice(0, 7).map(match => {
const title = match[1].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'");
const link = match[2];
return `- [${title}](${link})`;
const decode = (s) => s.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'");
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];

const posts = items.slice(0, 7).map(([, block]) => {
const grab = (tag) => (block.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`)) || [])[1] || '';
const title = decode(grab('title').trim());
const link = grab('link').trim();
const pubDate = grab('pubDate').trim();
const readingTime = decode(grab('blog:readingTime').trim());

const meta = [];
if (pubDate) {
const d = new Date(pubDate);
if (!isNaN(d)) meta.push(`${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`);
}
if (readingTime) meta.push(readingTime.replace(/ read$/, ''));

const suffix = meta.length ? ` β€” ${meta.join(' Β· ')}` : '';
return `- [${title}](${link})${suffix}`;
}).join('\n');

const readme = fs.readFileSync('README.md', 'utf8');
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

[![Typing SVG](https://readme-typing-svg.demolab.com?font=JetBrains+Mono&weight=700&size=32&duration=3000&pause=1000&color=7C3AED&center=true&vCenter=true&random=false&width=700&height=70&lines=Hey%2C+I'm+Esteban+Estrada;Software+Engineer+II+%40+Recurly;Building+tools+for+AI+agents;Rust+%7C+TypeScript+%7C+Go)](https://codestz.dev)
[![Typing SVG](https://readme-typing-svg.demolab.com?font=JetBrains+Mono&weight=700&size=32&duration=3000&pause=1000&color=7C3AED&center=true&vCenter=true&random=false&width=700&height=70&lines=Hey%2C+I'm+Esteban+Estrada;Senior+Software+Engineer+%40+Recurly;Building+tools+for+AI+agents;Rust+%7C+TypeScript+%7C+Go)](https://codestz.dev)

I build developer tools that make AI agents smarter.
<br/>Rust, TypeScript, Go β€” CLIs, MCP servers, LSP integrations.
Expand Down Expand Up @@ -86,6 +86,18 @@ I build developer tools that make AI agents smarter.

---

### Featured Project

<a href="https://github.com/Codestz/claude-hindsight">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./public/stats/repo-claude-hindsight-dark.svg"/>
<source media="(prefers-color-scheme: light)" srcset="./public/stats/repo-claude-hindsight-light.svg"/>
<img src="./public/stats/repo-claude-hindsight-dark.svg" alt="claude-hindsight"/>
</picture>
</a>

---

### Projects

[**krait**](https://github.com/Codestz/krait) -- Code intelligence CLI for AI agents. LSP-backed symbol search, semantic editing, and diagnostics in a single Rust binary. `rust`
Expand All @@ -107,6 +119,7 @@ I build developer tools that make AI agents smarter.
> I write about AI-driven development, MCP integrations, and developer tooling at **[codestz.dev](https://codestz.dev)**

<!-- BLOG-POST-LIST:START -->

- [Stop Vibe Coding. Start Vibe Engineering.](https://codestz.dev/experiments/stop-vibe-coding-start-vibe-engineering)
- [Stop Loading 100K Tokens Just to Call a Tool: Why I Built MCPX](https://codestz.dev/experiments/mcpx-mcp-gateway)
- [The AI Convergence: Why Every Coding Tool Lands on the Same Two Defaults](https://codestz.dev/experiments/why-ai-defaults-to-typescript)
Expand Down
10 changes: 0 additions & 10 deletions public/robots.txt

This file was deleted.

4 changes: 2 additions & 2 deletions public/stats/contributions-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions public/stats/contributions-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions public/stats/repo-claude-hindsight-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions public/stats/repo-claude-hindsight-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion public/stats/stats-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion public/stats/stats-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading