
Asset Manager
- 85 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
asset-manager is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- asset-manager
- AI & Agent Building
- AI-coding skill
Asset Manager by the numbers
- 85 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,034 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill asset-managerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 85 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Asset Manager
Manages design assets across projects: directory organization, naming conventions, image/font optimization, brand libraries, and version control. Use when assets need to be organized, compressed, converted to modern formats, or tracked across releases. Not for runtime image transformations or CDN configuration beyond path prefixing.
Quick Reference
| Task | Tool / Approach | Key Points |
|---|---|---|
| Image optimization | Sharp | Resize, compress, convert to WebP/AVIF/JPEG/PNG |
| SVG optimization | SVGO (v4+) | removeViewBox and removeTitle off by default in v4 |
| Font conversion | woff2_compress, sfnt2woff | TTF/OTF to WOFF2 (primary) + WOFF (fallback) |
| Font subsetting | Glyphhanger | Remove unused glyphs, auto-detect from crawled pages |
| Responsive images | Sharp breakpoints | Generate mobile (640), tablet (768), desktop (1920) variants |
| Asset versioning | SHA-256 hash tracking | asset-versions.json manifest with change detection |
| Large files in git | Git LFS | Track PSD, AI, Sketch, Figma, MP4, MOV files |
| Brand assets | Typed manifest | BrandAssets interface with logos, colors, typography |
Directory Structure
| Directory | Contents |
|---|---|
assets/images/{category}/ | Products, team, marketing, UI images |
assets/icons/svg/ | SVG icon files |
assets/fonts/{family}/ | WOFF2 + WOFF font files |
assets/videos/ | Video assets |
assets/logos/svg/, png/, variants/ | Logo formats and color variants |
brand/ | Colors JSON, typography JSON, guidelines |
Naming Conventions
| Asset Type | Pattern | Example |
|---|---|---|
| Images | {category}-{description}-{size}.{format} | product-hero-1920x1080.jpg |
| Icons | {icon-name}-{variant}.svg | home-outline.svg |
| Fonts | {font-family}-{weight}.{format} | Inter-Regular.woff2 |
| Logos | logo-{variant}.{format} | logo-full.svg, logo-white.svg |
Optimization Targets
| Format | Tool | Use Case |
|---|---|---|
| WebP | Sharp | Primary web images |
| AVIF | Sharp | Modern browsers, best compression |
| JPEG | Sharp (mozjpeg) | Fallback photos |
| PNG | Sharp | UI elements with transparency |
| SVG | SVGO | Icons and logos |
| WOFF2 | woff2_compress | Primary web fonts |
| WOFF | sfnt2woff | Font fallback |
Pipeline Steps
| Step | Action |
|---|---|
| Organize | Sort unsorted assets by naming rules into directories |
| Optimize images | Resize, compress, generate WebP/AVIF variants |
| Responsive images | Generate mobile/tablet/desktop breakpoint sizes |
| Optimize fonts | Convert TTF/OTF to WOFF2 + WOFF |
| Version | Hash-based tracking with asset-versions.json |
Common Mistakes
| Mistake | Fix |
|---|---|
| Committing raw design files to git | Use Git LFS for PSD, AI, Sketch, Figma, video files |
| Serving original-size images | Generate responsive variants at breakpoints |
| Using only JPEG/PNG | Generate WebP + AVIF with fallbacks |
| No font subsetting | Use Glyphhanger to subset unused glyphs |
Missing font-display: swap | Always set on @font-face to avoid FOIT |
| No CDN for assets | Prefix asset paths with CDN_URL env variable |
| Using imagemin for new projects | Use Sharp directly; imagemin is unmaintained |
| Using SVGO v3 plugin config with v4 | removeViewBox and removeTitle are off by default in v4 |
Delegation
- Discover asset organization patterns in a codebase: Use
Exploreagent to find existing asset directories, naming conventions, and optimization scripts - Optimize a batch of images or fonts: Use
Taskagent to run Sharp pipelines, font conversions, and responsive image generation - Plan a complete asset pipeline: Use
Planagent to design directory structure, naming conventions, optimization steps, and CI integration
References
- Organization
- Image Optimization
- Font Management
- Version Control
- Brand Library
- Best Practices
Best Practices
Format Selection
| Asset Type | Primary | Fallback |
|---|---|---|
| Photos | WebP / AVIF | JPEG |
| UI elements | WebP / AVIF | PNG |
| Icons | SVG sprite | Individual SVG |
| Fonts | WOFF2 | WOFF |
| Videos | MP4 (H.265) | MP4 (H.264) |
Lazy Loading
<img src="placeholder.jpg" data-src="hero.jpg" loading="lazy" alt="Hero" />With Next.js:
import Image from 'next/image';
<Image
src="/hero.jpg"
width={1920}
height={1080}
placeholder="blur"
alt="Hero"
/>;CDN Configuration
const CDN_URL = process.env.CDN_URL || '';
export function getAssetUrl(path: string): string {
if (CDN_URL) {
return `${CDN_URL}${path}`;
}
return path;
}Usage:
<img src={getAssetUrl('/images/hero.jpg')} alt="Hero" />Automated Pipeline
Orchestrate all optimization steps in sequence:
async function runAssetPipeline() {
await organizeAssets('./unsorted');
await optimizeImages('./assets/images/raw', './assets/images/optimized', {
quality: 85,
maxWidth: 1920,
formats: ['jpg', 'webp', 'avif'],
});
await generateResponsiveImages('./assets/images/optimized');
await optimizeFonts('./assets/fonts/raw', './assets/fonts/optimized');
const manager = new AssetVersionManager();
await manager.load();
await manager.trackDirectory('./assets');
await manager.save();
}Add to package.json:
{
"scripts": {
"assets:optimize": "tsx scripts/optimize-images.ts",
"assets:fonts": "tsx scripts/optimize-fonts.ts",
"assets:pipeline": "tsx scripts/asset-pipeline.ts"
}
}Brand Library
Brand Kit Structure
brand/
├── logos/
│ ├── primary/
│ │ ├── logo-full.svg
│ │ ├── logo-icon.svg
│ │ └── logo-wordmark.svg
│ ├── variations/
│ │ ├── logo-white.svg
│ │ ├── logo-black.svg
│ │ └── logo-inverted.svg
│ └── exports/
│ ├── png/
│ ├── pdf/
│ └── eps/
├── colors/
│ ├── colors.json
│ ├── colors.css
│ └── colors.scss
├── typography/
│ ├── fonts/
│ └── typography.json
└── guidelines/
├── brand-guidelines.pdf
├── logo-usage.pdf
└── color-usage.pdfBrand Asset Manifest
export interface BrandAssets {
version: string;
lastUpdated: string;
logos: LogoAsset[];
colors: ColorAsset[];
typography: TypographyAsset[];
}
export interface LogoAsset {
name: string;
variants: {
full: string;
icon: string;
wordmark: string;
};
formats: {
svg: string;
png: { [size: string]: string };
pdf: string;
};
}
export interface ColorAsset {
name: string;
hex: string;
rgb: { r: number; g: number; b: number };
usage: string;
}
export interface TypographyAsset {
name: string;
family: string;
weights: number[];
formats: string[];
}
export const brandAssets: BrandAssets = {
version: '2.0.0',
lastUpdated: '2024-01-15',
logos: [
{
name: 'Primary Logo',
variants: {
full: '/brand/logos/logo-full.svg',
icon: '/brand/logos/logo-icon.svg',
wordmark: '/brand/logos/logo-wordmark.svg',
},
formats: {
svg: '/brand/logos/logo-full.svg',
png: {
'1x': '/brand/logos/exports/png/logo-full@1x.png',
'2x': '/brand/logos/exports/png/logo-full@2x.png',
'3x': '/brand/logos/exports/png/logo-full@3x.png',
},
pdf: '/brand/logos/exports/pdf/logo-full.pdf',
},
},
],
colors: [
{
name: 'Primary',
hex: '#0066cc',
rgb: { r: 0, g: 102, b: 204 },
usage: 'Primary actions, links, brand elements',
},
],
typography: [
{
name: 'Inter',
family: 'Inter',
weights: [400, 600, 700],
formats: ['woff2', 'woff'],
},
],
};Font Management
Font Optimization
Convert TTF/OTF to WOFF2 (best compression) + WOFF (fallback):
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';
const execAsync = promisify(exec);
async function optimizeFonts(inputDir: string, outputDir: string) {
const files = await fs.readdir(inputDir);
for (const file of files) {
const inputPath = path.join(inputDir, file);
const ext = path.extname(file).toLowerCase();
if (ext !== '.ttf' && ext !== '.otf') continue;
const name = path.basename(file, ext);
const woff2Path = path.join(outputDir, `${name}.woff2`);
await convertToWOFF2(inputPath, woff2Path);
const woffPath = path.join(outputDir, `${name}.woff`);
await convertToWOFF(inputPath, woffPath);
}
}
async function convertToWOFF2(input: string, output: string) {
await execAsync(`woff2_compress ${input}`);
const woff2File = input.replace(/\.(ttf|otf)$/, '.woff2');
await fs.rename(woff2File, output);
}
async function convertToWOFF(input: string, output: string) {
await execAsync(`sfnt2woff ${input}`);
const woffFile = input.replace(/\.(ttf|otf)$/, '.woff');
await fs.rename(woffFile, output);
}Prerequisites: brew install woff2 for woff2_compress, sfnt2woff for WOFF conversion.
Font-Face Declarations
@font-face {
font-family: 'Inter';
src:
url('/fonts/Inter-Regular.woff2') format('woff2'),
url('/fonts/Inter-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src:
url('/fonts/Inter-Bold.woff2') format('woff2'),
url('/fonts/Inter-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}Always use font-display: swap to show fallback text immediately (avoids Flash of Invisible Text).
Preloading Critical Fonts
<head>
<link
rel="preload"
href="/fonts/Inter-Regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/Inter-Bold.woff2"
as="font"
type="font/woff2"
crossorigin
/>
</head>Only preload fonts used above the fold. The crossorigin attribute is required even for same-origin fonts.
Image Optimization
Dependencies
npm install sharp svgoMulti-Format Optimization
import sharp from 'sharp';
import fs from 'fs/promises';
import path from 'path';
interface OptimizeOptions {
quality?: number;
maxWidth?: number;
formats?: ('jpg' | 'png' | 'webp' | 'avif')[];
}
async function optimizeImages(
inputDir: string,
outputDir: string,
options: OptimizeOptions = {},
) {
const {
quality = 80,
maxWidth = 2000,
formats = ['jpg', 'png', 'webp'],
} = options;
const files = await fs.readdir(inputDir);
for (const file of files) {
const inputPath = path.join(inputDir, file);
const stat = await fs.stat(inputPath);
if (stat.isDirectory()) continue;
const ext = path.extname(file).toLowerCase();
const name = path.basename(file, ext);
if (ext === '.svg') {
await optimizeSVG(inputPath, outputDir);
continue;
}
if (!['.jpg', '.jpeg', '.png'].includes(ext)) continue;
const image = sharp(inputPath);
const metadata = await image.metadata();
if (metadata.width && metadata.width > maxWidth) {
image.resize(maxWidth, null, {
withoutEnlargement: true,
fit: 'inside',
});
}
for (const format of formats) {
const outputPath = path.join(outputDir, `${name}.${format}`);
if (format === 'jpg') {
await image.jpeg({ quality, mozjpeg: true }).toFile(outputPath);
} else if (format === 'png') {
await image.png({ quality, compressionLevel: 9 }).toFile(outputPath);
} else if (format === 'webp') {
await image.webp({ quality }).toFile(outputPath);
} else if (format === 'avif') {
await image.avif({ quality }).toFile(outputPath);
}
}
}
}SVG Optimization
SVGO v4 disables removeViewBox and removeTitle by default for better accessibility.
import { optimize } from 'svgo';
async function optimizeSVG(inputPath: string, outputDir: string) {
const fileName = path.basename(inputPath);
const outputPath = path.join(outputDir, fileName);
const svgString = await fs.readFile(inputPath, 'utf-8');
const result = optimize(svgString, {
multipass: true,
plugins: [
{
name: 'preset-default',
},
'removeDimensions',
],
});
await fs.writeFile(outputPath, result.data);
}Responsive Image Generation
const breakpoints = [
{ name: 'mobile', width: 640 },
{ name: 'tablet', width: 768 },
{ name: 'desktop', width: 1920 },
];
async function generateResponsiveImages(inputPath: string) {
const ext = path.extname(inputPath);
const name = path.basename(inputPath, ext);
const dir = path.dirname(inputPath);
for (const bp of breakpoints) {
const image = sharp(inputPath);
image.resize(bp.width, null, {
withoutEnlargement: true,
fit: 'inside',
});
const webpPath = path.join(dir, `${name}-${bp.name}.webp`);
await image.webp({ quality: 80 }).toFile(webpPath);
const avifPath = path.join(dir, `${name}-${bp.name}.avif`);
await image.avif({ quality: 80 }).toFile(avifPath);
}
}Organization
Directory Structure
assets/
├── images/
│ ├── products/
│ ├── team/
│ ├── marketing/
│ └── ui/
├── icons/
│ ├── svg/
│ └── png/
├── fonts/
│ ├── primary/
│ └── secondary/
├── videos/
├── logos/
│ ├── svg/
│ ├── png/
│ └── variants/
└── brand/
├── colors.json
├── typography.json
└── guidelines.pdfNaming Conventions
Images:
{category}-{description}-{size}.{format}
product-hero-1920x1080.jpg
team-sarah-400x400.jpg
ui-background-pattern.pngIcons:
{icon-name}-{variant}.svg
home-outline.svg
home-filled.svg
user-circle.svgFonts:
{font-family}-{weight}.{format}
Inter-Regular.woff2
Inter-Bold.woff2
Poppins-SemiBold.woff2Automated Organization
Pattern-based file sorting into directories:
import fs from 'fs/promises';
import path from 'path';
interface AssetRule {
pattern: RegExp;
destination: string;
}
const rules: AssetRule[] = [
{ pattern: /product-/i, destination: 'images/products' },
{ pattern: /team-/i, destination: 'images/team' },
{ pattern: /icon-/i, destination: 'icons/svg' },
{ pattern: /logo-/i, destination: 'logos' },
];
async function organizeAssets(sourceDir: string) {
const files = await fs.readdir(sourceDir);
for (const file of files) {
const sourcePath = path.join(sourceDir, file);
const stat = await fs.stat(sourcePath);
if (stat.isDirectory()) continue;
const rule = rules.find((r) => r.pattern.test(file));
if (rule) {
const destDir = path.join('assets', rule.destination);
await fs.mkdir(destDir, { recursive: true });
const destPath = path.join(destDir, file);
await fs.rename(sourcePath, destPath);
}
}
}Version Control
Git LFS Setup
Track large design files with Git LFS:
brew install git-lfs
git lfs install
git lfs track "*.psd"
git lfs track "*.ai"
git lfs track "*.sketch"
git lfs track "*.fig"
git lfs track "*.mp4"
git lfs track "*.mov"
git lfs track "assets/images/**/*.jpg"
git lfs track "assets/images/**/*.png"
git add .gitattributes
git commit -m "Configure Git LFS"Asset Versioning System
Track asset changes with SHA-256 hashing:
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
interface AssetVersion {
path: string;
hash: string;
size: number;
modified: string;
version: number;
}
class AssetVersionManager {
private versionFile = 'asset-versions.json';
private versions: Map<string, AssetVersion[]> = new Map();
async load() {
try {
const data = await fs.readFile(this.versionFile, 'utf-8');
const parsed = JSON.parse(data);
for (const [path, versions] of Object.entries(parsed)) {
this.versions.set(path, versions as AssetVersion[]);
}
} catch {
// File doesn't exist yet
}
}
async save() {
const data = Object.fromEntries(this.versions);
await fs.writeFile(this.versionFile, JSON.stringify(data, null, 2));
}
async trackAsset(filePath: string) {
const buffer = await fs.readFile(filePath);
const hash = crypto.createHash('sha256').update(buffer).digest('hex');
const stat = await fs.stat(filePath);
const versions = this.versions.get(filePath) || [];
const lastVersion = versions[versions.length - 1];
if (lastVersion && lastVersion.hash === hash) {
return;
}
versions.push({
path: filePath,
hash,
size: stat.size,
modified: stat.mtime.toISOString(),
version: versions.length + 1,
});
this.versions.set(filePath, versions);
}
async trackDirectory(dirPath: string) {
const files = await fs.readdir(dirPath, { recursive: true });
for (const file of files) {
const filePath = path.join(dirPath, file.toString());
const stat = await fs.stat(filePath);
if (stat.isDirectory()) continue;
await this.trackAsset(filePath);
}
}
}Usage:
const manager = new AssetVersionManager();
await manager.load();
await manager.trackDirectory('./assets');
await manager.save();