#!/usr/bin/env tsx /** * Obsidian Test Vault Generator * * This script generates a large number of markdown files with status tags * to test the performance of the Note Status plugin with large vaults. * * Usage: * npx tsx generate-test-vault.ts --count 40000 --output ./test-vault */ import * as fs from "fs"; import * as path from "path"; import { program } from "commander"; // Define command line options program .option("-c, --count ", "Number of notes to generate", "40000") .option("-o, --output ", "Output directory path", "./test-vault") .option( "-t, --tag-prefix ", "Status tag prefix", "obsidian-note-status", ) .option("-d, --depth ", "Maximum folder depth", "5") .option("-m, --max-per-folder ", "Maximum files per folder", "200") .option("-n, --no-status", "Generate notes without status tags") .parse(process.argv); const options = program.opts(); // Validate options const noteCount = parseInt(options.count, 10); const outputDir = options.output; const tagPrefix = options.tagPrefix; const maxDepth = parseInt(options.depth, 10); const maxPerFolder = parseInt(options.maxPerFolder, 10); const includeStatus = options.status !== false; // true by default, false if --no-status is used if (isNaN(noteCount) || noteCount <= 0) { console.error("Error: Note count must be a positive number"); process.exit(1); } // Define possible statuses (matching your plugin's defaults) const statuses = [ "active", "onHold", "completed", "dropped", "unknown", // Add some from the colorful template "idea", "draft", "inProgress", "editing", "pending", ]; // Define topics for more realistic note titles const topics = [ "Project", "Meeting", "Research", "Idea", "Book", "Journal", "Task", "Recipe", "Person", "Place", "Event", "Course", "Paper", "Review", "Analysis", "Summary", "Plan", "Design", "Budget", "Report", "Feature", "Bug", "Release", "Backlog", "Sprint", "Goal", "OKR", "KPI", "Metric", "Reflection", ]; // Generate a random status function getRandomStatus(): string { const multipleStatuses = Math.random() > 0.8; // 20% chance of multiple statuses if (multipleStatuses) { const count = Math.floor(Math.random() * 2) + 2; // 2-3 statuses const selectedStatuses = new Set(); while (selectedStatuses.size < count) { selectedStatuses.add( statuses[Math.floor(Math.random() * statuses.length)], ); } return JSON.stringify(Array.from(selectedStatuses)); } else { return JSON.stringify([ statuses[Math.floor(Math.random() * statuses.length)], ]); } } // Generate a random title function generateTitle(): string { const topic = topics[Math.floor(Math.random() * topics.length)]; const qualifier = Math.random() > 0.5 ? [ "New", "Important", "Urgent", "Weekly", "Monthly", "Daily", "Annual", ][Math.floor(Math.random() * 7)] : ""; const number = Math.random() > 0.3 ? Math.floor(Math.random() * 1000).toString() : ""; return [qualifier, topic, number].filter(Boolean).join(" ").trim(); } // Generate note content with random length function generateContent(): string { const paragraphCount = Math.floor(Math.random() * 5) + 1; let content = ""; for (let i = 0; i < paragraphCount; i++) { const sentenceCount = Math.floor(Math.random() * 5) + 1; let paragraph = ""; for (let j = 0; j < sentenceCount; j++) { const wordCount = Math.floor(Math.random() * 15) + 5; const sentence = Array(wordCount).fill("lorem").join(" ") + ". "; paragraph += sentence; } content += paragraph + "\n\n"; } return content; } // Create a note with optional frontmatter containing status function createNote(title: string, folderPath: string): void { const safeName = title.replace(/[^a-zA-Z0-9 ]/g, "").replace(/\s+/g, " "); const fileName = `${safeName}.md`; const filePath = path.join(folderPath, fileName); let fileContent = ""; if (includeStatus) { // Create frontmatter with random status const status = getRandomStatus(); fileContent = `---\n${tagPrefix}: ${status}\n---\n\n`; } // Create content fileContent += `# ${title}\n\n${generateContent()}`; // Write file fs.writeFileSync(filePath, fileContent); } // Generate a folder structure and notes function generateNotes(count: number, baseDir: string): void { // Create base directory if it doesn't exist if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir, { recursive: true }); } // Track created files let created = 0; let foldersCreated = 0; // Create a function to generate notes in a folder with recursion function generateNotesInFolder(folderPath: string, depth: number): void { if (created >= count) return; // Determine how many files to create in this folder const filesInThisFolder = Math.min( Math.floor(Math.random() * maxPerFolder) + 1, count - created, ); // Create files in this folder for (let i = 0; i < filesInThisFolder; i++) { if (created >= count) break; const title = generateTitle(); createNote(title, folderPath); created++; // Display progress if (created % 1000 === 0 || created === count) { console.log(`Created ${created}/${count} notes...`); } } // Possibly create subfolders if not at max depth if (depth < maxDepth && created < count) { // Decide how many subfolders to create const subfoldersCount = Math.floor(Math.random() * 5) + 1; for (let i = 0; i < subfoldersCount; i++) { if (created >= count) break; const folderName = `Folder ${++foldersCreated}`; const subfolderPath = path.join(folderPath, folderName); // Create subfolder if (!fs.existsSync(subfolderPath)) { fs.mkdirSync(subfolderPath); } // Recursively generate notes in subfolder generateNotesInFolder(subfolderPath, depth + 1); } } } // Start generation generateNotesInFolder(baseDir, 0); console.log( `\nSuccessfully created ${created} notes in ${foldersCreated} folders.`, ); console.log(`Output directory: ${path.resolve(baseDir)}`); } // Create .obsidian folder with minimal config to ensure it's recognized as a vault function createObsidianConfig(baseDir: string): void { const obsidianDir = path.join(baseDir, ".obsidian"); if (!fs.existsSync(obsidianDir)) { fs.mkdirSync(obsidianDir, { recursive: true }); } // Create a minimal config.json const configPath = path.join(obsidianDir, "config.json"); const config = { baseFontSize: 16, pluginEnabledStatus: { "file-explorer": true, "global-search": true, switcher: true, graph: true, backlink: true, "page-preview": true, templates: true, }, }; fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); // Create a minimal appearance.json const appearancePath = path.join(obsidianDir, "appearance.json"); const appearance = { baseFontSize: 16, theme: "obsidian", }; fs.writeFileSync(appearancePath, JSON.stringify(appearance, null, 2)); } // Main execution console.log( `Generating ${noteCount} notes in ${outputDir}${includeStatus ? ` with tag prefix '${tagPrefix}'` : " without status tags"}`, ); console.time("Generation completed in"); // Create test vault generateNotes(noteCount, outputDir); // Create Obsidian configuration createObsidianConfig(outputDir); console.timeEnd("Generation completed in"); console.log("\nInstructions:"); console.log("1. Open Obsidian"); console.log('2. Select "Open folder as vault"'); console.log(`3. Navigate to: ${path.resolve(outputDir)}`); console.log("4. Install and enable your Note Status plugin"); console.log("5. Test the plugin performance with this large vault");