Visibility internal Owner _ Approver _ Created _ Updated _
generate_view_pages.ts
| Field | Value |
|---|---|
| Type | TypeScript |
| Source | Forge/Skills/Manage_Pages/generate_view_pages.ts |
| Parent | Forge |
| GitHub | Forge/Skills/Manage_Pages/generate_view_pages.ts |
This page is auto-generated. Edit the source to change the content.
/**
* generate_folder_pages.ts
*
* Generates file-view pages, MEDIA.md pages,
* and sidebar addition reports for the Uvilo OS documentation site.
*
* Run with: npx tsx generate_folder_pages.ts
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
interface FolderConfig {
view_folder_patterns: string[];
supported_extensions: string[];
image_extensions: string[];
excluded_dirs: string[];
generated_dir: string;
max_file_lines: number;
}
interface IndexedFolder {
absPath: string;
name: string;
relPath: string;
}
interface ScannedFile {
absPath: string;
relPath: string;
folderRelPath: string;
ext: string;
fileName: string;
}
interface MediaFile {
absPath: string;
relPath: string;
folderRelPath: string;
fileName: string;
ext: string;
}
function getFileType(ext: string): string {
const map: Record<string, string> = {
'.ts': 'TypeScript', '.tsx': 'TypeScript',
'.js': 'JavaScript', '.jsx': 'JavaScript',
'.json': 'JSON', '.yaml': 'YAML', '.yml': 'YAML',
'.py': 'Python', '.sh': 'Shell', '.sql': 'SQL',
'.txt': 'Plain Text', '.csv': 'CSV', '.css': 'CSS', '.html': 'HTML',
'.md': 'Markdown',
};
return map[ext] || ext.slice(1).toUpperCase();
}
function getCodeBlockLang(ext: string): string {
const map: Record<string, string> = {
'.ts': 'typescript', '.tsx': 'typescript',
'.js': 'javascript', '.jsx': 'javascript',
'.json': 'json', '.yaml': 'yaml', '.yml': 'yaml',
'.py': 'python', '.sh': 'bash', '.sql': 'sql',
'.txt': 'text', '.csv': 'text', '.css': 'css', '.html': 'html',
'.md': 'markdown',
};
return map[ext] || '';
}
function isExcludedDir(dirName: string, excludedDirs: string[]): boolean {
return excludedDirs.includes(dirName);
}
function isSupportedExt(ext: string, extensions: string[]): boolean {
return extensions.includes(ext);
}
function isImageExt(ext: string, extensions: string[]): boolean {
return extensions.includes(ext);
}
function githubRawUrl(relPath: string): string {
return `https://github.com/ErikDakoda/uvilo-os/blob/dev/${relPath}`;
}
function loadConfig(repoRoot: string): FolderConfig {
const configPath = path.join(
repoRoot, 'Forge', 'Skills', 'Manage_Pages', 'view_folder_config.json'
);
const raw = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(raw) as FolderConfig;
}
function expandPatterns(repoRoot: string, patterns: string[], excludedDirs: string[]): IndexedFolder[] {
const folders: IndexedFolder[] = [];
const seen = new Set<string>();
for (const pattern of patterns) {
const absPattern = path.join(repoRoot, pattern);
const dirs = fs.globSync(absPattern.endsWith('/') ? absPattern : absPattern + '/', {
withFileTypes: true,
exclude: (entry) => entry.name.startsWith('.') || excludedDirs.includes(entry.name),
});
for (const dir of dirs) {
if (!dir.isDirectory()) continue;
const relPath = path.relative(repoRoot, dir.parentPath ? path.join(dir.parentPath, dir.name) : dir.path);
if (seen.has(relPath)) continue;
seen.add(relPath);
folders.push({
absPath: path.join(repoRoot, relPath),
name: path.basename(relPath),
relPath,
});
}
}
return folders;
}
function scanFolders(repoRoot: string, config: FolderConfig): IndexedFolder[] {
return expandPatterns(repoRoot, config.view_folder_patterns, config.excluded_dirs);
}
function generateFileViewPage(file: ScannedFile, folder: IndexedFolder, config: FolderConfig): string {
const content = fs.readFileSync(file.absPath, 'utf-8');
const lines = content.split('\n');
const fileType = getFileType(file.ext);
const codeLang = getCodeBlockLang(file.ext);
const depth = file.folderRelPath.split(path.sep).length;
const backLink = '../'.repeat(depth) + (file.relPath.includes('Skills') ? 'skill/' : 'readme/');
let fileContentBlock: string;
if (lines.length > config.max_file_lines) {
const truncated = lines.slice(0, config.max_file_lines).join('\n');
fileContentBlock = `\`\`\`${codeLang}\n${truncated}\n\`\`\`\n\n`;
fileContentBlock += `> **Note:** This file exceeds ${config.max_file_lines} lines. Showing the first ${config.max_file_lines} lines.\n`;
fileContentBlock += `> [View the complete file on GitHub](${githubRawUrl(file.relPath)})\n`;
} else {
fileContentBlock = `\`\`\`${codeLang}\n${content}\n\`\`\`\n`;
}
return `---
title: "${file.fileName}"
generated: true
---
# ${file.fileName}
<br><br>
| Field | Value |
|---|---|
| **Type** | ${fileType} |
| **Source** | \`${file.relPath}\` |
| **Parent** | [${folder.name}](${backLink}) |
| **GitHub** | [${file.relPath}](${githubRawUrl(file.relPath)}) |
This page is auto-generated. Edit the source to change the content.
<br><br>
${fileContentBlock}
`;
}
function scanForNonMdFiles(folder: IndexedFolder, config: FolderConfig): ScannedFile[] {
const files: ScannedFile[] = [];
function walk(dirPath: string, relDir: string): void {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
if (isExcludedDir(entry.name, config.excluded_dirs)) continue;
const fullPath = path.join(dirPath, entry.name);
const fullRelPath = path.join(relDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, fullRelPath);
} else {
const ext = path.extname(entry.name).toLowerCase();
if (ext === '.md') continue;
if (isSupportedExt(ext, config.supported_extensions)) {
files.push({
absPath: fullPath,
relPath: fullRelPath,
folderRelPath: path.relative(folder.absPath, fullPath),
ext,
fileName: entry.name,
});
}
}
}
}
walk(folder.absPath, folder.relPath);
return files;
}
function scanDirForMedia(dirPath: string, imageExts: string[], excludedDirs: string[]): boolean {
if (!fs.existsSync(dirPath)) return false;
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.') || entry.isDirectory()) continue;
const ext = path.extname(entry.name).toLowerCase();
if (isImageExt(ext, imageExts)) return true;
}
} catch {
return false;
}
return false;
}
function scanForMediaFolders(folder: IndexedFolder, config: FolderConfig): Map<string, MediaFile[]> {
const folders = new Map<string, MediaFile[]>();
function walk(dirPath: string, relDir: string): void {
let hasMedia = false;
const mediaFiles: MediaFile[] = [];
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) continue;
if (isExcludedDir(entry.name, config.excluded_dirs)) continue;
const fullPath = path.join(dirPath, entry.name);
const fullRelPath = path.join(relDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, fullRelPath);
} else {
const ext = path.extname(entry.name).toLowerCase();
if (isImageExt(ext, config.image_extensions)) {
hasMedia = true;
mediaFiles.push({
absPath: fullPath,
relPath: fullRelPath,
folderRelPath: path.relative(folder.absPath, fullPath),
fileName: entry.name,
ext,
});
}
}
}
} catch {
return;
}
if (hasMedia && dirPath !== folder.absPath) {
folders.set(dirPath, mediaFiles);
}
}
walk(folder.absPath, folder.relPath);
return folders;
}
function generateMediaMd(
folderPath: string,
mediaFiles: MediaFile[],
folder: IndexedFolder
): string {
const folderRelPath = path.relative(folder.absPath, folderPath);
const folderName = path.basename(folderPath);
const displayName = folder.name.replace(/_/g, ' ');
let md = `---\n`;
md += `title: "${displayName} — ${folderName}"\n`;
md += `visibility: internal\n`;
md += `generated: true\n`;
md += `---\n\n`;
md += `# ${displayName} — ${folderName}\n\n`;
md += `> Path: \`${folder.relPath}/${folderRelPath.replace(/\\/g, '/')}\`\n\n`;
for (const media of mediaFiles) {
// Relative paths for image display (raw.githubusercontent.com 404s on this private repo).
const relUrl = `./${media.fileName}`;
// GitHub blob URL for download link (the relative path breaks in the built site).
const downloadUrl = `https://github.com/ErikDakoda/uvilo-os/blob/dev/${media.relPath}`;
md += `## ${media.fileName}\n\n`;
md += `\n\n`;
md += `[Download](${downloadUrl})\n\n`;
}
return md;
}
function generateSidebarReport(
folders: IndexedFolder[],
nonMdFilesMap: Map<string, ScannedFile[]>,
mediaFoldersMap: Map<string, Map<string, MediaFile[]>>,
config: FolderConfig
): string {
let report = `# Sidebar Additions Report\n\n`;
report += `> Generated by generate_view_pages.ts\n`;
report += `> Review each section and add the suggested slugs to astro.config.mjs\n\n`;
for (const folder of folders) {
const folderSlug = folder.relPath.replace(/\\/g, '/').toLowerCase();
const nonMdFiles = nonMdFilesMap.get(folder.absPath) || [];
const mediaFolders = mediaFoldersMap.get(folder.absPath) || new Map();
report += `## ${folder.name}\n\n`;
const filesByDir = new Map<string, ScannedFile[]>();
for (const file of nonMdFiles) {
const dir = path.dirname(file.folderRelPath);
const files = filesByDir.get(dir) || [];
files.push(file);
filesByDir.set(dir, files);
}
if (filesByDir.size > 0) {
report += `### File-view pages\n\n`;
for (const [dir, files] of filesByDir) {
const dirSlug = dir === '.' ? folderSlug : `${folderSlug}/${dir.replace(/\\/g, '/').toLowerCase()}`;
const dirLabel = dir === '.' ? folder.name : path.basename(dir);
report += `**${dirLabel}** (${dir === '.' ? 'root' : dir}):\n\`\`\`js\n`;
for (const file of files) {
const fileViewSlug = `${dirSlug}/${file.fileName}.view`;
report += `{ slug: '${fileViewSlug}' },\n`;
}
report += `\`\`\`\n\n`;
}
}
if (mediaFolders.size > 0) {
report += `### MEDIA.md pages\n\n`;
for (const [folderPath] of mediaFolders) {
const folderRelPath = path.relative(folder.absPath, folderPath);
const mediaSlug = `${folderSlug}/${folderRelPath.replace(/\\/g, '/').toLowerCase()}/media`;
report += `**${folderRelPath}**:\n\`\`\`js\n`;
report += `{ slug: '${mediaSlug}' },\n`;
report += `\`\`\`\n\n`;
}
}
report += `---\n\n`;
}
return report;
}
function main(): void {
const scriptDir = __dirname;
const repoRoot = path.resolve(scriptDir, '..', '..', '..');
console.log(`Repo root: ${repoRoot}`);
const config = loadConfig(repoRoot);
console.log(`Config loaded. View folder patterns: ${config.view_folder_patterns.join(', ')}`);
const folders = scanFolders(repoRoot, config);
console.log(`Found ${folders.length} folders:`);
for (const p of folders) {
console.log(` - ${p.name}`);
}
const generatedBase = path.join(repoRoot, config.generated_dir);
if (!fs.existsSync(generatedBase)) {
fs.mkdirSync(generatedBase, { recursive: true });
}
const nonMdFilesMap = new Map<string, ScannedFile[]>();
const mediaFoldersMap = new Map<string, Map<string, MediaFile[]>>();
for (const folder of folders) {
console.log(`\nProcessing: ${folder.name}`);
const nonMdFiles = scanForNonMdFiles(folder, config);
nonMdFilesMap.set(folder.absPath, nonMdFiles);
for (const file of nonMdFiles) {
const relativeToFolder = path.relative(folder.absPath, file.absPath);
const generatedRelPath = path.join(
config.generated_dir,
folder.relPath,
relativeToFolder + '.view.md'
);
const generatedAbsPath = path.join(repoRoot, generatedRelPath);
fs.mkdirSync(path.dirname(generatedAbsPath), { recursive: true });
const viewContent = generateFileViewPage(file, folder, config);
fs.writeFileSync(generatedAbsPath, viewContent);
}
console.log(` ✓ ${nonMdFiles.length} file-view page(s) generated`);
const mediaFolders = scanForMediaFolders(folder, config);
mediaFoldersMap.set(folder.absPath, mediaFolders);
for (const [folderPath, mediaFiles] of mediaFolders) {
const mediaMdPath = path.join(folderPath, 'MEDIA.md');
const mediaContent = generateMediaMd(folderPath, mediaFiles, folder);
fs.writeFileSync(mediaMdPath, mediaContent);
console.log(` ✓ MEDIA.md written to ${path.relative(folder.absPath, folderPath)}`);
}
}
const sidebarReport = generateSidebarReport(folders, nonMdFilesMap, mediaFoldersMap, config);
const reportPath = path.join(generatedBase, 'sidebar-additions.md');
fs.writeFileSync(reportPath, sidebarReport);
console.log(`\n✓ Sidebar additions report: ${reportPath}`);
console.log('\nDone!');
}
main();