diff --git a/.github/workflows/techstack.yml b/.github/workflows/techstack.yml
new file mode 100644
index 0000000..c39a393
--- /dev/null
+++ b/.github/workflows/techstack.yml
@@ -0,0 +1,30 @@
+name: Update Tech Stack
+
+on:
+ schedule:
+ - cron: "0 6 * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ update:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Generate tech stack badges
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: node scripts/update-techstack.js
+
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: "chore: update tech stack badges [skip ci]"
+ file_pattern: README.md
diff --git a/README.md b/README.md
index 28221ee..98714b8 100644
--- a/README.md
+++ b/README.md
@@ -15,20 +15,13 @@
## 🛠️ Tech Stack
+> *Berdasarkan repositori dan aktivitas akun ini*
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+ Memuat tech stack dari repositori...
+
## 📊 GitHub Analytics
diff --git a/scripts/update-techstack.js b/scripts/update-techstack.js
new file mode 100644
index 0000000..37a7b82
--- /dev/null
+++ b/scripts/update-techstack.js
@@ -0,0 +1,241 @@
+// scripts/update-techstack.js
+// Fetches repos and topics from GitHub API -> generates shields.io badges -> updates README markers
+const https = require("https");
+const fs = require("fs");
+const path = require("path");
+
+// -- Configuration ----------------------------------------------
+const GITHUB_USER = "asepharyana";
+const GITHUB_TOKEN = process.env.GITHUB_TOKEN || "";
+const README_PATH = path.join(__dirname, "..", "README.md");
+const MARKER_START = "";
+const MARKER_END = "";
+const MAX_BADGES = 16;
+
+// Map of known repo topics/languages -> shields.io badge config
+const BADGE_MAP = {
+ // Languages
+ Go: { label: "Go", color: "00ADD8", logo: "go" },
+ Python: { label: "Python", color: "3776AB", logo: "python" },
+ TypeScript: { label: "TypeScript", color: "3178C6", logo: "typescript" },
+ JavaScript: { label: "JavaScript", color: "F7DF1E", logo: "javascript", logoColor: "black" },
+ Rust: { label: "Rust", color: "000000", logo: "rust" },
+ Java: { label: "Java", color: "ED8B00", logo: "openjdk" },
+ Kotlin: { label: "Kotlin", color: "7F52FF", logo: "kotlin" },
+ C: { label: "C", color: "A8B9CC", logo: "c" },
+ "C++": { label: "C++", color: "00599C", logo: "cplusplus" },
+ "C#": { label: "C#", color: "239120", logo: "csharp" },
+ PHP: { label: "PHP", color: "777BB4", logo: "php" },
+ Ruby: { label: "Ruby", color: "CC342D", logo: "ruby" },
+ Swift: { label: "Swift", color: "F05138", logo: "swift" },
+ Dart: { label: "Dart", color: "0175C2", logo: "dart" },
+ Lua: { label: "Lua", color: "2C2D72", logo: "lua" },
+ Shell: { label: "Shell", color: "4EAA25", logo: "gnubash" },
+ HTML: { label: "HTML", color: "E34F26", logo: "html5" },
+ CSS: { label: "CSS", color: "1572B6", logo: "css3" },
+ SCSS: { label: "SCSS", color: "CC6699", logo: "sass" },
+ Jupyter: { label: "Jupyter", color: "F37626", logo: "jupyter" },
+ Dockerfile: { label: "Docker", color: "2496ED", logo: "docker" },
+ Solidity: { label: "Solidity", color: "363636", logo: "solidity" },
+ Zig: { label: "Zig", color: "F7A41D", logo: "zig" },
+
+ // Frameworks & Tools (detected from topics or repo context)
+ react: { label: "React", color: "61DAFB", logo: "react", logoColor: "black" },
+ nextjs: { label: "Next.js", color: "000000", logo: "nextdotjs" },
+ "next.js": { label: "Next.js", color: "000000", logo: "nextdotjs" },
+ vue: { label: "Vue.js", color: "4FC08D", logo: "vuedotjs" },
+ svelte: { label: "Svelte", color: "FF3E00", logo: "svelte" },
+ angular: { label: "Angular", color: "DD0031", logo: "angular" },
+ tailwindcss: { label: "Tailwind", color: "06B6D4", logo: "tailwindcss" },
+ "tailwind css": { label: "Tailwind", color: "06B6D4", logo: "tailwindcss" },
+ nodejs: { label: "Node.js", color: "5FA04E", logo: "nodedotjs" },
+ "node.js": { label: "Node.js", color: "5FA04E", logo: "nodedotjs" },
+ express: { label: "Express", color: "000000", logo: "express" },
+ django: { label: "Django", color: "092E20", logo: "django" },
+ flask: { label: "Flask", color: "000000", logo: "flask" },
+ fastapi: { label: "FastAPI", color: "009688", logo: "fastapi" },
+ spring: { label: "Spring", color: "6DB33F", logo: "spring" },
+ laravel: { label: "Laravel", color: "FF2D20", logo: "laravel" },
+ rails: { label: "Rails", color: "D30001", logo: "rubyonrails" },
+ "ruby on rails": { label: "Rails", color: "D30001", logo: "rubyonrails" },
+ dotnet: { label: ".NET", color: "512BD4", logo: "dotnet" },
+ flutter: { label: "Flutter", color: "02569B", logo: "flutter" },
+ tensorflow: { label: "TensorFlow", color: "FF6F00", logo: "tensorflow" },
+ pytorch: { label: "PyTorch", color: "EE4C2C", logo: "pytorch" },
+ "machine-learning": { label: "ML/AI", color: "01BFFF", logo: "tensorflow" }, // generic ML
+ "deep-learning": { label: "Deep Learning", color: "EE4C2C", logo: "pytorch" },
+ "natural-language-processing":
+ { label: "NLP", color: "01BFFF", logo: "googlenatural language" },
+ "large-language-model":
+ { label: "LLM", color: "412991", logo: "googlegemini" },
+ llm: { label: "LLM", color: "412991", logo: "googlegemini" },
+ langchain: { label: "LangChain", color: "1C3C3C", logo: "langchain" },
+ rag: { label: "RAG", color: "FF6B35", logo: "googlegemini" },
+ openai: { label: "OpenAI", color: "412991", logo: "openai" },
+ ai: { label: "AI", color: "667eea", logo: "googlegemini" },
+ docker: { label: "Docker", color: "2496ED", logo: "docker" },
+ kubernetes: { label: "Kubernetes", color: "326CE5", logo: "kubernetes" },
+ k8s: { label: "Kubernetes", color: "326CE5", logo: "kubernetes" },
+ terraform: { label: "Terraform", color: "844FBA", logo: "terraform" },
+ postgresql: { label: "PostgreSQL", color: "4169E1", logo: "postgresql" },
+ postgres: { label: "PostgreSQL", color: "4169E1", logo: "postgresql" },
+ mysql: { label: "MySQL", color: "4479A1", logo: "mysql" },
+ mongodb: { label: "MongoDB", color: "47A248", logo: "mongodb" },
+ redis: { label: "Redis", color: "FF4438", logo: "redis" },
+ graphql: { label: "GraphQL", color: "E10098", logo: "graphql" },
+ grpc: { label: "gRPC", color: "244C5E", logo: "grpc" },
+ nginx: { label: "Nginx", color: "009639", logo: "nginx" },
+ githubactions: { label: "GitHub Actions", color: "2088FF", logo: "githubactions" },
+ "ci/cd": { label: "CI/CD", color: "2088FF", logo: "githubactions" },
+ linux: { label: "Linux", color: "FCC624", logo: "linux", logoColor: "black" },
+ git: { label: "Git", color: "F05032", logo: "git" },
+};
+
+// -- Helpers ----------------------------------------------------
+
+function fetch(url, options = {}) {
+ return new Promise((resolve, reject) => {
+ const req = https.get(url, { headers: options.headers || {} }, (res) => {
+ let data = "";
+ res.on("data", (chunk) => (data += chunk));
+ res.on("end", () => resolve({ status: res.statusCode, data }));
+ });
+ req.on("error", reject);
+ req.setTimeout(15000, () => { req.destroy(); reject(new Error("Request timeout")); });
+ });
+}
+
+function fetchAllPages(url, token) {
+ const results = [];
+ const headers = {
+ "User-Agent": "update-techstack",
+ Accept: "application/vnd.github.v3+json",
+ };
+ if (token) headers.Authorization = `Bearer ${token}`;
+
+ return (function next(pageUrl) {
+ return fetch(pageUrl, { headers }).then(({ status, data }) => {
+ if (status !== 200) {
+ console.log(`GitHub API returned ${status}, stopping pagination`);
+ return results;
+ }
+ const page = JSON.parse(data);
+ results.push(...page);
+ // Check for Link header to find next page
+ // We don't have direct header access in our simple fetch, so we use a different approach:
+ // parse the response array length as signal
+ if (page.length === 100) {
+ const urlObj = new URL(pageUrl);
+ const perPage = parseInt(urlObj.searchParams.get("per_page") || "100");
+ const currentPage = parseInt(urlObj.searchParams.get("page") || "1");
+ if (page.length >= perPage) {
+ urlObj.searchParams.set("page", String(currentPage + 1));
+ return next(urlObj.toString());
+ }
+ }
+ return results;
+ });
+ })(url);
+}
+
+function toBadgeHtml(cfg) {
+ const logoColor = cfg.logoColor ? `&logoColor=${cfg.logoColor}` : "";
+ return `
`;
+}
+
+// -- Main -------------------------------------------------------
+
+async function main() {
+ console.log("[TechStack] Starting...");
+
+ if (!GITHUB_TOKEN) {
+ console.log("[TechStack] No GITHUB_TOKEN, keeping existing content");
+ return;
+ }
+
+ // 1. Fetch all repos (up to 100 per page, auto-paginate)
+ const reposUrl = `https://api.github.com/users/${GITHUB_USER}/repos?per_page=100&sort=updated&type=all`;
+ const repos = await fetchAllPages(reposUrl, GITHUB_TOKEN);
+
+ if (!repos || repos.length === 0) {
+ console.log("[TechStack] No repos found");
+ return;
+ }
+
+ console.log(`[TechStack] Found ${repos.length} repos`);
+
+ // 2. Aggregate languages with byte counts from each repo
+ const langByteMap = {};
+ const topicSet = new Set();
+
+ for (const repo of repos) {
+ // Collect topics
+ const topics = repo.topics || [];
+ for (const t of topics) topicSet.add(t.toLowerCase());
+
+ // Collect primary language
+ if (repo.language) {
+ const lang = repo.language;
+ langByteMap[lang] = (langByteMap[lang] || 0) + 1; // count repos using this language
+ }
+ }
+
+ // 3. Build tech set: languages + topic-detected tools
+ const techSet = new Map(); // key -> {cfg, priority}
+
+ // Add languages first (sorted by repo count)
+ const sortedLangs = Object.entries(langByteMap).sort((a, b) => b[1] - a[1]);
+ for (const [lang] of sortedLangs) {
+ if (BADGE_MAP[lang]) {
+ techSet.set(lang, { cfg: BADGE_MAP[lang], priority: 1 });
+ }
+ }
+
+ // Add topic-detected technologies
+ for (const topic of topicSet) {
+ if (BADGE_MAP[topic]) {
+ const key = BADGE_MAP[topic].label;
+ // Don't replace a language badge with a topic badge of same label
+ if (!techSet.has(key) || techSet.get(key).priority > 0) {
+ techSet.set(key, { cfg: BADGE_MAP[topic], priority: 0 });
+ }
+ }
+ }
+
+ // 4. Sort: first languages (priority 1), then topic-detected, limit to MAX_BADGES
+ const sorted = [...techSet.entries()]
+ .sort((a, b) => a[1].priority - b[1].priority)
+ .slice(0, MAX_BADGES);
+
+ // 5. Generate HTML
+ const badges = sorted.map(([, v]) => toBadgeHtml(v.cfg)).join("\n ");
+ const techHtml = `\n ${badges}\n
`;
+
+ // 6. Update README
+ if (!fs.existsSync(README_PATH)) {
+ console.error(`[TechStack] README.md not found at ${README_PATH}`);
+ return;
+ }
+
+ let readme = fs.readFileSync(README_PATH, "utf-8");
+ const startIdx = readme.indexOf(MARKER_START);
+ const endIdx = readme.indexOf(MARKER_END);
+
+ if (startIdx === -1 || endIdx === -1) {
+ console.error("[TechStack] Section markers not found in README.md");
+ return;
+ }
+
+ const before = readme.slice(0, startIdx + MARKER_START.length);
+ const after = readme.slice(endIdx);
+ const middle = `\n\n${techHtml}\n\n`;
+ const newReadme = before + middle + after;
+
+ fs.writeFileSync(README_PATH, newReadme, "utf-8");
+ console.log(`[TechStack] README.md updated with ${sorted.length} badges`);
+}
+
+main().catch((err) => {
+ console.error(`[TechStack] Error: ${err.message}`);
+ process.exit(0);
+});