Merge pull request #649 from github/feature/last-updated-dates

feat(website): Add last updated dates for resources
This commit is contained in:
Aaron Powell
2026-02-04 08:30:02 +11:00
committed by GitHub
14 changed files with 360 additions and 15 deletions

View File

@@ -24,6 +24,7 @@ import {
parseSkillMetadata,
parseYamlFile,
} from "./yaml-parser.mjs";
import { getGitFileDates } from "./utils/git-dates.mjs";
const __filename = fileURLToPath(import.meta.url);
@@ -64,7 +65,7 @@ function extractTitle(filePath, frontmatter) {
/**
* Generate agents metadata
*/
function generateAgentsData() {
function generateAgentsData(gitDates) {
const agents = [];
const files = fs
.readdirSync(AGENTS_DIR)
@@ -105,6 +106,7 @@ function generateAgentsData() {
: [],
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}
@@ -123,7 +125,7 @@ function generateAgentsData() {
/**
* Generate prompts metadata
*/
function generatePromptsData() {
function generatePromptsData(gitDates) {
const prompts = [];
const files = fs
.readdirSync(PROMPTS_DIR)
@@ -151,6 +153,7 @@ function generatePromptsData() {
tools: tools,
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}
@@ -206,7 +209,7 @@ function extractExtensionFromPattern(pattern) {
/**
* Generate instructions metadata
*/
function generateInstructionsData() {
function generateInstructionsData(gitDates) {
const instructions = [];
const files = fs
.readdirSync(INSTRUCTIONS_DIR)
@@ -253,6 +256,7 @@ function generateInstructionsData() {
extensions: [...new Set(extensions)],
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}
@@ -316,7 +320,7 @@ function categorizeSkill(name, description) {
/**
* Generate skills metadata
*/
function generateSkillsData() {
function generateSkillsData(gitDates) {
const skills = [];
if (!fs.existsSync(SKILLS_DIR)) {
@@ -343,6 +347,9 @@ function generateSkillsData() {
// Get all files in the skill folder recursively
const files = getSkillFiles(skillPath, relativePath);
// Get last updated from SKILL.md file
const skillFilePath = `${relativePath}/SKILL.md`;
skills.push({
id: folder,
name: metadata.name,
@@ -356,8 +363,9 @@ function generateSkillsData() {
assetCount: metadata.assets.length,
category: category,
path: relativePath,
skillFile: `${relativePath}/SKILL.md`,
skillFile: skillFilePath,
files: files,
lastUpdated: gitDates.get(skillFilePath) || null,
});
}
}
@@ -406,7 +414,7 @@ function getSkillFiles(skillPath, relativePath) {
/**
* Generate collections metadata
*/
function generateCollectionsData() {
function generateCollectionsData(gitDates) {
const collections = [];
if (!fs.existsSync(COLLECTIONS_DIR)) {
@@ -447,6 +455,7 @@ function generateCollectionsData() {
})),
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}
}
@@ -542,6 +551,7 @@ function generateSearchIndex(
title: agent.title,
description: agent.description,
path: agent.path,
lastUpdated: agent.lastUpdated,
searchText: `${agent.title} ${agent.description} ${agent.tools.join(
" "
)}`.toLowerCase(),
@@ -555,6 +565,7 @@ function generateSearchIndex(
title: prompt.title,
description: prompt.description,
path: prompt.path,
lastUpdated: prompt.lastUpdated,
searchText: `${prompt.title} ${prompt.description}`.toLowerCase(),
});
}
@@ -566,6 +577,7 @@ function generateSearchIndex(
title: instruction.title,
description: instruction.description,
path: instruction.path,
lastUpdated: instruction.lastUpdated,
searchText: `${instruction.title} ${instruction.description} ${
instruction.applyTo || ""
}`.toLowerCase(),
@@ -579,6 +591,7 @@ function generateSearchIndex(
title: skill.title,
description: skill.description,
path: skill.skillFile,
lastUpdated: skill.lastUpdated,
searchText: `${skill.title} ${skill.description}`.toLowerCase(),
});
}
@@ -591,6 +604,7 @@ function generateSearchIndex(
description: collection.description,
path: collection.path,
tags: collection.tags,
lastUpdated: collection.lastUpdated,
searchText: `${collection.name} ${
collection.description
} ${collection.tags.join(" ")}`.toLowerCase(),
@@ -703,32 +717,40 @@ async function main() {
ensureDataDir();
// Load git dates for all resource files (single efficient git command)
console.log("Loading git history for last updated dates...");
const gitDates = getGitFileDates(
["agents/", "prompts/", "instructions/", "skills/", "collections/"],
ROOT_FOLDER
);
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
// Generate all data
const agentsData = generateAgentsData();
const agentsData = generateAgentsData(gitDates);
const agents = agentsData.items;
console.log(
`✓ Generated ${agents.length} agents (${agentsData.filters.models.length} models, ${agentsData.filters.tools.length} tools)`
);
const promptsData = generatePromptsData();
const promptsData = generatePromptsData(gitDates);
const prompts = promptsData.items;
console.log(
`✓ Generated ${prompts.length} prompts (${promptsData.filters.tools.length} tools)`
);
const instructionsData = generateInstructionsData();
const instructionsData = generateInstructionsData(gitDates);
const instructions = instructionsData.items;
console.log(
`✓ Generated ${instructions.length} instructions (${instructionsData.filters.extensions.length} extensions)`
);
const skillsData = generateSkillsData();
const skillsData = generateSkillsData(gitDates);
const skills = skillsData.items;
console.log(
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
);
const collectionsData = generateCollectionsData();
const collectionsData = generateCollectionsData(gitDates);
const collections = collectionsData.items;
console.log(
`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`

103
eng/utils/git-dates.mjs Normal file
View File

@@ -0,0 +1,103 @@
#!/usr/bin/env node
/**
* Utility to extract last modification dates from git history.
* Uses a single git log command for efficiency.
*/
import { execSync } from "child_process";
import path from "path";
/**
* Get the last modification date for all tracked files in specified directories.
* Returns a Map of file path -> ISO date string.
*
* @param {string[]} directories - Array of directory paths to scan
* @param {string} rootDir - Root directory for relative paths
* @returns {Map<string, string>} Map of relative file path to ISO date string
*/
export function getGitFileDates(directories, rootDir) {
const fileDates = new Map();
try {
// Get git log with file names for all specified directories
// Format: ISO date, then file names that were modified in that commit
const gitArgs = [
"--no-pager",
"log",
"--format=%aI", // Author date in ISO 8601 format
"--name-only",
"--diff-filter=ACMR", // Added, Copied, Modified, Renamed
"--",
...directories,
];
const output = execSync(`git ${gitArgs.join(" ")}`, {
encoding: "utf8",
cwd: rootDir,
stdio: ["pipe", "pipe", "pipe"],
});
// Parse the output: alternating date lines and file name lines
// Format is:
// 2026-01-15T10:30:00+00:00
//
// file1.md
// file2.md
//
// 2026-01-14T09:00:00+00:00
// ...
let currentDate = null;
const lines = output.split("\n");
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
// Check if this is a date line (ISO 8601 format)
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
currentDate = trimmed;
} else if (currentDate && trimmed) {
// This is a file path - only set if we haven't seen this file yet
// (first occurrence is the most recent modification)
if (!fileDates.has(trimmed)) {
fileDates.set(trimmed, currentDate);
}
}
}
} catch (error) {
// Git command failed - might not be a git repo or no history
console.warn("Warning: Could not get git dates:", error.message);
}
return fileDates;
}
/**
* Get the last modification date for a single file.
*
* @param {string} filePath - Path to the file (relative to git root)
* @param {string} rootDir - Root directory
* @returns {string|null} ISO date string or null if not found
*/
export function getGitFileDate(filePath, rootDir) {
try {
const output = execSync(
`git --no-pager log -1 --format="%aI" -- "${filePath}"`,
{
encoding: "utf8",
cwd: rootDir,
stdio: ["pipe", "pipe", "pipe"],
}
);
const date = output.trim();
return date || null;
} catch (error) {
return null;
}
}