11#!/usr/bin/env python3
2- """Generate manifest.json from skill directories."""
2+ """Generate or validate manifest.json from skill directories."""
33
4+ import argparse
45import json
56import re
7+ import sys
68from datetime import datetime , timezone
79from pathlib import Path
810
@@ -54,7 +56,9 @@ def generate_manifest(repo_root: Path) -> dict:
5456 """Generate manifest from skill directories."""
5557 skills = {}
5658
57- for item in sorted (repo_root .iterdir ()):
59+ skills_dir = repo_root / "skills"
60+
61+ for item in sorted (skills_dir .iterdir ()):
5862 if not item .is_dir ():
5963 continue
6064 if item .name .startswith ("." ) or item .name == "scripts" :
@@ -76,15 +80,72 @@ def generate_manifest(repo_root: Path) -> dict:
7680 }
7781
7882
79- def main () -> None :
80- repo_root = Path (__file__ ).parent .parent
81- manifest = generate_manifest (repo_root )
83+ def normalize_manifest (manifest : dict ) -> dict :
84+ """Normalize manifest for comparison by excluding updated_at timestamps."""
85+ normalized = manifest .copy ()
86+ normalized .pop ("updated_at" , None )
87+
88+ skills = {}
89+ for name , skill in manifest .get ("skills" , {}).items ():
90+ skill_copy = skill .copy ()
91+ skill_copy .pop ("updated_at" , None )
92+ skills [name ] = skill_copy
8293
94+ normalized ["skills" ] = skills
95+ return normalized
96+
97+
98+ def validate_manifest (repo_root : Path ) -> bool :
99+ """Validate that manifest.json is up to date. Returns True if valid."""
83100 manifest_path = repo_root / "manifest.json"
84- manifest_path .write_text (json .dumps (manifest , indent = 2 ) + "\n " )
85101
86- print (f"Generated { manifest_path } " )
87- print (f"Found { len (manifest ['skills' ])} skill(s): { ', ' .join (manifest ['skills' ].keys ())} " )
102+ if not manifest_path .exists ():
103+ print ("ERROR: manifest.json does not exist" , file = sys .stderr )
104+ return False
105+
106+ current_manifest = json .loads (manifest_path .read_text ())
107+ expected_manifest = generate_manifest (repo_root )
108+
109+ # compare without timestamps
110+ current_normalized = normalize_manifest (current_manifest )
111+ expected_normalized = normalize_manifest (expected_manifest )
112+
113+ if current_normalized != expected_normalized :
114+ print ("ERROR: manifest.json is out of date" , file = sys .stderr )
115+ print ("\n Expected:" , file = sys .stderr )
116+ print (json .dumps (expected_normalized , indent = 2 ), file = sys .stderr )
117+ print ("\n Actual:" , file = sys .stderr )
118+ print (json .dumps (current_normalized , indent = 2 ), file = sys .stderr )
119+ return False
120+
121+ print ("manifest.json is up to date" )
122+ return True
123+
124+
125+ def main () -> None :
126+ parser = argparse .ArgumentParser (description = "Generate or validate manifest.json" )
127+ parser .add_argument (
128+ "mode" ,
129+ nargs = "?" ,
130+ default = "generate" ,
131+ choices = ["generate" , "validate" ],
132+ help = "Mode: generate (creates manifest.json, default) or validate (checks if up to date)" ,
133+ )
134+
135+ args = parser .parse_args ()
136+ repo_root = Path (__file__ ).parent .parent
137+
138+ match args .mode :
139+ case "generate" :
140+ manifest = generate_manifest (repo_root )
141+ manifest_path = repo_root / "manifest.json"
142+ manifest_path .write_text (json .dumps (manifest , indent = 2 ) + "\n " )
143+ print (f"Generated { manifest_path } " )
144+ print (f"Found { len (manifest ['skills' ])} skill(s): { ', ' .join (manifest ['skills' ].keys ())} " )
145+
146+ case "validate" :
147+ is_valid = validate_manifest (repo_root )
148+ sys .exit (0 if is_valid else 1 )
88149
89150
90151if __name__ == "__main__" :
0 commit comments