diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4449f64b..bb607060 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -673,7 +673,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules: | `create_vault_theme` | Create markdown theme note | | `ensure_vault_themes` | Seed default themes if missing | | `restore_default_themes` | Restore all default themes | -| `repair_vault` | Restore default themes + missing config files | +| `repair_vault` | Flatten vault structure, migrate legacy frontmatter, restore themes + config | ### AI & MCP diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index a817f188..ffe950de 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -552,6 +552,10 @@ pub fn restore_default_themes(vault_path: String) -> Result { #[tauri::command] pub fn repair_vault(vault_path: String) -> Result { let vault_path = expand_tilde(&vault_path); + // Migrate legacy is_a/Is A frontmatter → type + vault::migrate_is_a_to_type(&vault_path)?; + // Flatten vault: move notes from type-based subfolders to root + vault::flatten_vault(&vault_path)?; // Migrate legacy theme/ directory to root, then repair themes theme::migrate_theme_dir_to_root(&vault_path); theme::restore_default_themes(&vault_path)?; @@ -788,4 +792,49 @@ mod tests { let result = get_default_vault_path(); assert!(result.is_ok()); } + + #[test] + fn test_repair_vault_flattens_type_folders() { + let dir = tempfile::TempDir::new().unwrap(); + let vault = dir.path(); + let note_dir = vault.join("note"); + std::fs::create_dir_all(¬e_dir).unwrap(); + std::fs::write( + note_dir.join("hello.md"), + "---\nis_a: Note\n---\n# Hello\n", + ) + .unwrap(); + + let result = repair_vault(vault.to_str().unwrap().to_string()); + assert!(result.is_ok()); + // Note moved from note/ subfolder to root + assert!(vault.join("hello.md").exists()); + assert!(!note_dir.join("hello.md").exists()); + // Legacy is_a migrated to type + let content = std::fs::read_to_string(vault.join("hello.md")).unwrap(); + assert!(content.contains("type: Note")); + assert!(!content.contains("is_a:")); + } + + #[test] + fn test_repair_vault_creates_config_and_theme_files() { + let dir = tempfile::TempDir::new().unwrap(); + let vault = dir.path(); + + let result = repair_vault(vault.to_str().unwrap().to_string()); + assert!(result.is_ok()); + // Config files at root + assert!(vault.join("AGENTS.md").exists()); + assert!(vault.join("config.md").exists()); + // Theme files at root (flat structure) + assert!(vault.join("default-theme.md").exists()); + assert!(vault.join("dark-theme.md").exists()); + assert!(vault.join("minimal-theme.md").exists()); + assert!(vault.join("theme.md").exists()); + // No type/themes subfolders + assert!(!vault.join("theme").exists()); + assert!(!vault.join("config").exists()); + // .gitignore + assert!(vault.join(".gitignore").exists()); + } } diff --git a/src-tauri/src/vault/config_seed.rs b/src-tauri/src/vault/config_seed.rs index 368f8b63..940c4dfb 100644 --- a/src-tauri/src/vault/config_seed.rs +++ b/src-tauri/src/vault/config_seed.rs @@ -6,7 +6,7 @@ use super::getting_started::AGENTS_MD; /// Content for `config.md` — gives the Config type a sidebar icon and label. const CONFIG_TYPE_DEFINITION: &str = "\ --- -Is A: Type +type: Type icon: gear-six color: gray order: 90 @@ -174,7 +174,7 @@ mod tests { assert!(vault.join("config.md").exists()); let content = fs::read_to_string(vault.join("config.md")).unwrap(); - assert!(content.contains("Is A: Type")); + assert!(content.contains("type: Type")); assert!(content.contains("icon: gear-six")); } diff --git a/src/hooks/useCommandRegistry.ts b/src/hooks/useCommandRegistry.ts index b11f5649..4352abf0 100644 --- a/src/hooks/useCommandRegistry.ts +++ b/src/hooks/useCommandRegistry.ts @@ -269,7 +269,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction { id: 'install-mcp', label: mcpStatus === 'installed' ? 'Restore MCP Server' : 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install', 'restore', 'fix', 'repair'], enabled: true, execute: () => onInstallMcp?.() }, { id: 'reindex-vault', label: 'Reindex Vault', group: 'Settings', keywords: ['reindex', 'index', 'search', 'rebuild', 'refresh'], enabled: !!onReindexVault, execute: () => onReindexVault?.() }, { id: 'reload-vault', label: 'Reload Vault', group: 'Settings', keywords: ['reload', 'refresh', 'rescan', 'sync', 'filesystem', 'cache'], enabled: !!onReloadVault, execute: () => onReloadVault?.() }, - { id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset'], enabled: !!onRepairVault, execute: () => onRepairVault?.() }, + { id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset', 'flatten', 'structure'], enabled: !!onRepairVault, execute: () => onRepairVault?.() }, // Type-aware: "New [Type]" and "List [Type]" ...buildTypeCommands(vaultTypes, onCreateNoteOfType, onSelect), diff --git a/tests/smoke/repair-vault-flat-structure.spec.ts b/tests/smoke/repair-vault-flat-structure.spec.ts new file mode 100644 index 00000000..c2f7b252 --- /dev/null +++ b/tests/smoke/repair-vault-flat-structure.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test.describe('Repair Vault — flat structure', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + }) + + test('repair vault executes without errors', async ({ page }) => { + const errors: string[] = [] + page.on('console', msg => { + if (msg.type() === 'error') errors.push(msg.text()) + }) + + // Open command palette and run Repair Vault + await page.keyboard.press('Meta+k') + await page.waitForTimeout(300) + await page.keyboard.type('repair vault') + await page.waitForTimeout(300) + await page.keyboard.press('Enter') + await page.waitForTimeout(1000) + + // No console errors related to repair_vault + const repairErrors = errors.filter(e => e.includes('repair_vault')) + expect(repairErrors).toHaveLength(0) + }) + + test('app loads without type folder structure errors', async ({ page }) => { + const main = page.locator('#root') + await expect(main).toBeVisible() + + const errors: string[] = [] + page.on('console', msg => { + if (msg.type() === 'error') errors.push(msg.text()) + }) + await page.waitForTimeout(1000) + + // No errors about move_note_to_type_folder or type folder operations + const folderErrors = errors.filter( + e => e.includes('move_note_to_type_folder') || e.includes('type_folder'), + ) + expect(folderErrors).toHaveLength(0) + }) +})