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
3 changes: 2 additions & 1 deletion .codespellrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# categor - TypeScript template literal in website/src/scripts/pages/skills.ts:70 (categor${...length > 1 ? "ies" : "y"})
# aline - proper name (Aline Ávila, contributor)
# ative - part of "Declarative Agents" in TypeSpec M365 Copilot documentation (collections/typespec-m365-copilot.collection.md)
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques
# dateA, dateB - variable names used in sorting comparison functions
ignore-words-list = numer,wit,aks,edn,ser,ois,gir,rouge,categor,aline,ative,afterall,deques,dateA,dateB
# Skip certain files and directories
skip = .git,node_modules,package-lock.json,*.lock,website/build,website/.docusaurus
2 changes: 2 additions & 0 deletions .github/workflows/deploy-website.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for git-based last updated dates

- name: Setup Node.js
uses: actions/setup-node@v4
Expand Down
44 changes: 33 additions & 11 deletions eng/generate-website-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
parseSkillMetadata,
parseYamlFile,
} from "./yaml-parser.mjs";
import { getGitFileDates } from "./utils/git-dates.mjs";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down Expand Up @@ -65,7 +66,7 @@ function extractTitle(filePath, frontmatter) {
/**
* Generate agents metadata
*/
function generateAgentsData() {
function generateAgentsData(gitDates) {
const agents = [];
const files = fs
.readdirSync(AGENTS_DIR)
Expand Down Expand Up @@ -106,6 +107,7 @@ function generateAgentsData() {
: [],
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}

Expand All @@ -124,7 +126,7 @@ function generateAgentsData() {
/**
* Generate prompts metadata
*/
function generatePromptsData() {
function generatePromptsData(gitDates) {
const prompts = [];
const files = fs
.readdirSync(PROMPTS_DIR)
Expand Down Expand Up @@ -152,6 +154,7 @@ function generatePromptsData() {
tools: tools,
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}

Expand Down Expand Up @@ -207,7 +210,7 @@ function extractExtensionFromPattern(pattern) {
/**
* Generate instructions metadata
*/
function generateInstructionsData() {
function generateInstructionsData(gitDates) {
const instructions = [];
const files = fs
.readdirSync(INSTRUCTIONS_DIR)
Expand Down Expand Up @@ -254,6 +257,7 @@ function generateInstructionsData() {
extensions: [...new Set(extensions)],
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}

Expand Down Expand Up @@ -317,7 +321,7 @@ function categorizeSkill(name, description) {
/**
* Generate skills metadata
*/
function generateSkillsData() {
function generateSkillsData(gitDates) {
const skills = [];

if (!fs.existsSync(SKILLS_DIR)) {
Expand All @@ -344,6 +348,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,
Expand All @@ -357,8 +364,9 @@ function generateSkillsData() {
assetCount: metadata.assets.length,
category: category,
path: relativePath,
skillFile: `${relativePath}/SKILL.md`,
skillFile: skillFilePath,
files: files,
lastUpdated: gitDates.get(skillFilePath) || null,
});
}
}
Expand Down Expand Up @@ -407,7 +415,7 @@ function getSkillFiles(skillPath, relativePath) {
/**
* Generate collections metadata
*/
function generateCollectionsData() {
function generateCollectionsData(gitDates) {
const collections = [];

if (!fs.existsSync(COLLECTIONS_DIR)) {
Expand Down Expand Up @@ -448,6 +456,7 @@ function generateCollectionsData() {
})),
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
});
}
}
Expand Down Expand Up @@ -543,6 +552,7 @@ function generateSearchIndex(
title: agent.title,
description: agent.description,
path: agent.path,
lastUpdated: agent.lastUpdated,
searchText: `${agent.title} ${agent.description} ${agent.tools.join(
" "
)}`.toLowerCase(),
Expand All @@ -556,6 +566,7 @@ function generateSearchIndex(
title: prompt.title,
description: prompt.description,
path: prompt.path,
lastUpdated: prompt.lastUpdated,
searchText: `${prompt.title} ${prompt.description}`.toLowerCase(),
});
}
Expand All @@ -567,6 +578,7 @@ function generateSearchIndex(
title: instruction.title,
description: instruction.description,
path: instruction.path,
lastUpdated: instruction.lastUpdated,
searchText: `${instruction.title} ${instruction.description} ${
instruction.applyTo || ""
}`.toLowerCase(),
Expand All @@ -580,6 +592,7 @@ function generateSearchIndex(
title: skill.title,
description: skill.description,
path: skill.skillFile,
lastUpdated: skill.lastUpdated,
searchText: `${skill.title} ${skill.description}`.toLowerCase(),
});
}
Expand All @@ -592,6 +605,7 @@ function generateSearchIndex(
description: collection.description,
path: collection.path,
tags: collection.tags,
lastUpdated: collection.lastUpdated,
searchText: `${collection.name} ${
collection.description
} ${collection.tags.join(" ")}`.toLowerCase(),
Expand Down Expand Up @@ -704,32 +718,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)`
Expand Down
103 changes: 103 additions & 0 deletions eng/utils/git-dates.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
}
8 changes: 8 additions & 0 deletions website/public/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,14 @@ a:hover {
flex-shrink: 0;
}

/* Last Updated */
.last-updated {
font-size: 12px;
color: var(--color-text-muted);
cursor: default;
margin-left: auto;
}

/* Collection Items */
.collection-items {
margin-top: 12px;
Expand Down
7 changes: 7 additions & 0 deletions website/src/pages/agents.astro
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ import Modal from '../components/Modal.astro';
Has Handoffs
</label>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>

Expand Down
7 changes: 7 additions & 0 deletions website/src/pages/instructions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import Modal from '../components/Modal.astro';
<label for="filter-extension">File Extension:</label>
<select id="filter-extension" multiple aria-label="Filter by file extension"></select>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>

Expand Down
7 changes: 7 additions & 0 deletions website/src/pages/prompts.astro
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import Modal from '../components/Modal.astro';
<label for="filter-tool">Tool:</label>
<select id="filter-tool" multiple aria-label="Filter by tool"></select>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>

Expand Down
7 changes: 7 additions & 0 deletions website/src/pages/skills.astro
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ import Modal from '../components/Modal.astro';
Has Bundled Assets
</label>
</div>
<div class="filter-group">
<label for="sort-select">Sort:</label>
<select id="sort-select" aria-label="Sort by">
<option value="title">Name (A-Z)</option>
<option value="lastUpdated">Recently Updated</option>
</select>
</div>
<button id="clear-filters" class="btn btn-secondary btn-small">Clear Filters</button>
</div>

Expand Down
Loading