Motivazione: - Node.js già installato (v25.7.0), zero privilegi necessari - Nessuna dipendenza npm richiesta (usa built-in modules) - Tool di sistema per estrazione: unzip, tar, identify (ImageMagick) - Più gestibile in ambienti senza sudo Cambiamenti: - extract_archive.py → extract_archive.js (11.6KB) - Usa execSync per unzip/tar/unrar - Stessa logica, zero dipendenze esterne - scan_resources.py → scan_resources.js (13.4KB) - Usa ImageMagick identify per metadata immagini - ffprobe opzionale per video - Genera tag e use case automaticamente - generate_catalog.py → generate_catalog.js (8.7KB) - Stesso output markdown - Zero dipendenze - README.md aggiornato con comandi Node.js - SKILL.md aggiornato con riferimenti corretti Dipendenze opzionali (tool di sistema): - unrar: Supporto archivi RAR - ffmpeg/ffprobe: Metadata video avanzati
295 lines
8.5 KiB
JavaScript
Executable file
295 lines
8.5 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
/**
|
|
* generate_catalog.js — Genera catalogo markdown dai metadata
|
|
*
|
|
* Usage:
|
|
* node generate_catalog.js --client <client_name>
|
|
* node generate_catalog.js --client demo_co_srl
|
|
*
|
|
* Options:
|
|
* --input Path metadata JSON
|
|
* --output Path output catalog.md
|
|
* --verbose Log dettagliato
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
|
|
function formatSize(bytes) {
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let size = bytes;
|
|
for (const unit of units) {
|
|
if (size < 1024) return `${size.toFixed(1)} ${unit}`;
|
|
size /= 1024;
|
|
}
|
|
return `${size.toFixed(1)} TB`;
|
|
}
|
|
|
|
function loadMetadata(inputPath) {
|
|
const content = fs.readFileSync(inputPath, 'utf8');
|
|
return JSON.parse(content);
|
|
}
|
|
|
|
function groupByType(resources) {
|
|
const grouped = {
|
|
images: [],
|
|
videos: [],
|
|
documents: [],
|
|
other: []
|
|
};
|
|
|
|
for (const res of resources) {
|
|
const mime = res.mime_type || '';
|
|
const ext = res.extension || '';
|
|
|
|
if (mime.startsWith('image/')) {
|
|
grouped.images.push(res);
|
|
} else if (mime.startsWith('video/')) {
|
|
grouped.videos.push(res);
|
|
} else if (['pdf', 'doc', 'docx', 'txt', 'md', 'ppt', 'pptx', 'xls', 'xlsx'].includes(ext)) {
|
|
grouped.documents.push(res);
|
|
} else {
|
|
grouped.other.push(res);
|
|
}
|
|
}
|
|
|
|
return grouped;
|
|
}
|
|
|
|
function generateSummary(grouped) {
|
|
const rows = [];
|
|
|
|
for (const [type, label] of [['images', 'Immagini'], ['videos', 'Video'], ['documents', 'Documenti']]) {
|
|
const resources = grouped[type] || [];
|
|
const count = resources.length;
|
|
const totalSize = resources.reduce((sum, r) => sum + (r.size_bytes || 0), 0);
|
|
|
|
rows.push(`| ${label} | ${count} | ${formatSize(totalSize)} |`);
|
|
}
|
|
|
|
return rows.join('\n');
|
|
}
|
|
|
|
function generateImagesTable(resources) {
|
|
if (resources.length === 0) return '_Nessuna immagine trovata_';
|
|
|
|
const rows = [];
|
|
const header = '| File | Tipo | Dimensioni | Risoluzione | Descrizione | Tag | Use Case |';
|
|
const separator = '|------|------|------------|-------------|-------------|-----|----------|';
|
|
|
|
const sorted = [...resources].sort((a, b) => a.filename.localeCompare(b.filename));
|
|
|
|
for (const res of sorted) {
|
|
const filename = res.filename || 'Unknown';
|
|
const ext = (res.extension || '?').toUpperCase();
|
|
const size = res.size_formatted || '?';
|
|
const resolution = res.resolution || '-';
|
|
const description = res.description || filename;
|
|
const tags = (res.tags || []).slice(0, 5).map(t => `#${t}`).join(', ');
|
|
const useCases = (res.use_cases || []).slice(0, 2).join(', ');
|
|
|
|
rows.push(`| \`${filename}\` | ${ext} | ${size} | ${resolution} | ${description} | ${tags} | ${useCases} |`);
|
|
}
|
|
|
|
return [header, separator, ...rows].join('\n');
|
|
}
|
|
|
|
function generateVideosTable(resources) {
|
|
if (resources.length === 0) return '_Nessun video trovato_';
|
|
|
|
const rows = [];
|
|
const header = '| File | Tipo | Dimensioni | Durata | Risoluzione | Descrizione | Tag | Use Case |';
|
|
const separator = '|------|------|------------|--------|-------------|-------------|-----|----------|';
|
|
|
|
const sorted = [...resources].sort((a, b) => a.filename.localeCompare(b.filename));
|
|
|
|
for (const res of sorted) {
|
|
const filename = res.filename || 'Unknown';
|
|
const ext = (res.extension || '?').toUpperCase();
|
|
const size = res.size_formatted || '?';
|
|
const resolution = res.resolution || '-';
|
|
const duration = res.duration ? `${Math.floor(res.duration / 60)}:${(res.duration % 60).toFixed(0).padStart(2, '0')}` : '-';
|
|
const description = res.description || filename;
|
|
const tags = (res.tags || []).slice(0, 5).map(t => `#${t}`).join(', ');
|
|
const useCases = (res.use_cases || []).slice(0, 2).join(', ');
|
|
|
|
rows.push(`| \`{filename}\` | ${ext} | ${size} | ${duration} | ${resolution} | ${description} | ${tags} | ${useCases} |`);
|
|
}
|
|
|
|
return [header, separator, ...rows].join('\n');
|
|
}
|
|
|
|
function generateDocumentsTable(resources) {
|
|
if (resources.length === 0) return '_Nessun documento trovato_';
|
|
|
|
const rows = [];
|
|
const header = '| File | Tipo | Dimensioni | Descrizione | Tag | Use Case |';
|
|
const separator = '|------|------|------------|-------------|-----|----------|';
|
|
|
|
const sorted = [...resources].sort((a, b) => a.filename.localeCompare(b.filename));
|
|
|
|
for (const res of sorted) {
|
|
const filename = res.filename || 'Unknown';
|
|
const ext = (res.extension || '?').toUpperCase();
|
|
const size = res.size_formatted || '?';
|
|
const description = res.description || filename;
|
|
const tags = (res.tags || []).slice(0, 5).map(t => `#${t}`).join(', ');
|
|
const useCases = (res.use_cases || []).slice(0, 2).join(', ');
|
|
|
|
rows.push(`| \`${filename}\` | ${ext} | ${size} | ${description} | ${tags} | ${useCases} |`);
|
|
}
|
|
|
|
return [header, separator, ...rows].join('\n');
|
|
}
|
|
|
|
function generateGlobalTags(resources) {
|
|
const allTags = new Set();
|
|
|
|
for (const res of resources) {
|
|
for (const tag of (res.tags || [])) {
|
|
allTags.add(tag);
|
|
}
|
|
}
|
|
|
|
if (allTags.size === 0) return '_Nessun tag generato_';
|
|
|
|
const sortedTags = [...allTags].sort().slice(0, 20);
|
|
return sortedTags.map(t => `#${t}`).join(' ');
|
|
}
|
|
|
|
function generateCatalog(clientName, metadata, outputPath, verbose = false) {
|
|
const resources = metadata.resources || [];
|
|
const generated = (metadata.generated || new Date().toISOString()).split('T')[0];
|
|
|
|
const grouped = groupByType(resources);
|
|
|
|
const catalog = `# Asset Catalog — ${clientName.replace(/_/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
|
|
|
|
_Generato: ${generated} | Totale: ${resources.length} risorse_
|
|
|
|
## Riepilogo
|
|
|
|
| Tipo | Count | Dimensione Totale |
|
|
|------|-------|-------------------|
|
|
${generateSummary(grouped)}
|
|
|
|
---
|
|
|
|
## Immagini (${grouped.images.length})
|
|
|
|
${generateImagesTable(grouped.images)}
|
|
|
|
---
|
|
|
|
## Video (${grouped.videos.length})
|
|
|
|
${generateVideosTable(grouped.videos)}
|
|
|
|
---
|
|
|
|
## Documenti (${grouped.documents.length})
|
|
|
|
${generateDocumentsTable(grouped.documents)}
|
|
|
|
---
|
|
|
|
## Tag Globali
|
|
|
|
${generateGlobalTags(resources)}
|
|
|
|
---
|
|
|
|
## Note
|
|
|
|
- **Ultimo aggiornamento:** ${generated}
|
|
- **Archivi originali:** \`assets/archive/\`
|
|
- **Per richiedere risorse:** Contatta @agency-archivist
|
|
- **Metadata completi:** \`assets/.metadata.json\`
|
|
`;
|
|
|
|
fs.writeFileSync(outputPath, catalog, 'utf8');
|
|
|
|
if (verbose) {
|
|
console.log(`✅ Catalogo generato: ${outputPath}`);
|
|
}
|
|
|
|
return outputPath;
|
|
}
|
|
|
|
function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
let client = null;
|
|
let inputPath = null;
|
|
let outputPath = null;
|
|
let verbose = false;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--client' && args[i + 1]) {
|
|
client = args[++i];
|
|
} else if (args[i] === '--input' && args[i + 1]) {
|
|
inputPath = args[++i];
|
|
} else if (args[i] === '--output' && args[i + 1]) {
|
|
outputPath = args[++i];
|
|
} else if (args[i] === '--verbose') {
|
|
verbose = true;
|
|
}
|
|
}
|
|
|
|
if (!client) {
|
|
console.error('Usage: node generate_catalog.js --client <client_name>');
|
|
console.error('Options: --input, --output, --verbose');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Path
|
|
const workspace = path.join(os.homedir(), '.openclaw', 'workspace', 'agency-skills-suite');
|
|
const clientDir = path.join(workspace, 'clients', client);
|
|
const assetsDir = path.join(clientDir, 'assets');
|
|
|
|
if (!fs.existsSync(clientDir)) {
|
|
console.error(`❌ Cartella cliente non trovata: ${clientDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!fs.existsSync(assetsDir)) {
|
|
console.error(`❌ Cartella assets non trovata: ${assetsDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Input/Output path
|
|
if (!inputPath) {
|
|
inputPath = path.join(assetsDir, '.metadata.json');
|
|
}
|
|
if (!outputPath) {
|
|
outputPath = path.join(assetsDir, 'catalog.md');
|
|
}
|
|
|
|
if (!fs.existsSync(inputPath)) {
|
|
console.error(`❌ Metadata non trovati: ${inputPath}`);
|
|
console.error(' Esegui prima: node scripts/scan_resources.js');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (verbose) {
|
|
console.log(`📥 Input: ${inputPath}`);
|
|
console.log(`📝 Output: ${outputPath}`);
|
|
console.log();
|
|
}
|
|
|
|
// Carica metadata
|
|
const metadata = loadMetadata(inputPath);
|
|
|
|
// Genera catalogo
|
|
generateCatalog(client, metadata, outputPath, verbose);
|
|
|
|
// Riepilogo
|
|
const resources = metadata.resources || [];
|
|
console.log(`\n✅ Catalogo generato!`);
|
|
console.log(` 📊 Risorse catalogate: ${resources.length}`);
|
|
console.log(` 📁 Catalogo: ${outputPath}`);
|
|
console.log(`\n👉 Il catalogo è pronto per essere usato dalle altre skill!`);
|
|
}
|
|
|
|
main();
|