feat: support hidden file sync and expand binary extension list

- Use FileSystemAdapter.list() recursively to discover hidden files (e.g. .claude)
- Fix ensureParentDirs to use adapter.mkdir() for hidden directory creation
- Expand BINARY_EXTENSIONS with modern image, audio, video, archive, and design formats
- Exclude .agents/** from ESLint to prevent project-service parse errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ClaudiaFang 2026-05-22 03:08:15 +00:00
parent 556f9e930d
commit 649d732781
7 changed files with 42 additions and 126 deletions

View file

@ -1,61 +0,0 @@
---
name: obsidian-development
description: Use when developing Obsidian plugins to ensure TDD with Vitest, linting, and adherence to Obsidian API conventions. Trigger this skill when the user mentions building, testing, or modifying an Obsidian plugin, or when they ask for help with Obsidian-specific APIs (commands, ribbon icons, workspace events).
---
# Obsidian Plugin Development
You are a specialist in building Obsidian plugins. This skill ensures you follow the project's specific conventions for testing, linting, and API usage.
## Core Principles
1. **Test-Driven Development (TDD)**: Always write tests before implementation for logic and service classes.
2. **API Hygiene**: Use Obsidian's lifecycle methods correctly (`onload`, `onunload`) and register all events/intervals for automatic cleanup.
3. **Linting**: Adhere to the project's ESLint configuration (`npm run lint`).
## Development Workflow
### 1. Testing with Vitest
The project uses `vitest` for unit and integration testing. Tests are located in the `tests/` directory.
- **Command**: `npm run test` or `npm run test:ui` for the interactive dashboard.
- **Pattern**: Create a corresponding `.test.ts` file for every logic or service file.
- **Example**:
```typescript
// tests/logic/sync-manager.test.ts
import { describe, it, expect, vi } from 'vitest';
// ... test implementation
```
### 2. Obsidian API Conventions
- **Settings**: Define settings in `src/settings.ts`. Use `this.loadData()` and `this.saveData()` in the main plugin class.
- **Commands**: Register commands using `this.addCommand()`. Always provide an `id` and `name`.
- **UI Elements**: Use `this.addRibbonIcon()` for sidebar buttons.
- **Cleanup**:
- Use `this.registerEvent()` for workspace events.
- Use `this.registerDomEvent()` for DOM events.
- Use `this.registerInterval()` for recurring tasks.
- **Why**: These ensure that when the plugin is disabled or uninstalled, Obsidian automatically cleans up the resources to prevent memory leaks.
### 3. Build & Deployment
- **Dev Mode**: `npm run dev` (watches for changes and rebuilds).
- **Production Build**: `npm run build` (runs type checking and minification).
- **Versioning**: `npm run version` (bumps version in `manifest.json` and updates `versions.json`).
## Project Structure Reference
- `src/main.ts`: Entry point (Main Plugin Class).
- `src/settings.ts`: Settings definitions and UI tab.
- `src/services/`: External integrations (e.g., GitLab API).
- `src/logic/`: Core business logic.
- `src/ui/`: Custom modals, views, or setting components.
## Checklist for New Features
- [ ] Draft test cases in `tests/` before writing logic.
- [ ] Use `registerEvent` instead of raw `on` handlers where possible.
- [ ] Verify `npm run lint` passes before completion.
- [ ] Ensure all persistent data is saved via `this.saveSettings()`.

View file

@ -1,17 +0,0 @@
{
"skill_name": "obsidian-development",
"evals": [
{
"id": 1,
"prompt": "Add a new command to the plugin that allows users to manually check for GitLab updates for the current file. Make sure it follows the project's testing and API conventions.",
"expected_output": "The model should first create a test case for the new logic, then implement the command in src/main.ts using this.addCommand(), and ensure any events are registered correctly.",
"files": ["src/main.ts", "src/logic/sync-manager.ts"]
},
{
"id": 2,
"prompt": "I need a way to see the current sync status in the status bar. How should I implement this?",
"expected_output": "The model should recommend using this.addStatusBarItem() and registering any update events with this.registerEvent() to ensure cleanup.",
"files": ["src/main.ts"]
}
]
}

View file

@ -1,35 +0,0 @@
# Pull Request Template
## Template
```markdown
# I am submitting a new Community Plugin
- [ ] I attest that I have done my best to deliver a high-quality plugin, am proud of the code I have written, and would recommend it to others. I commit to maintaining the plugin and being responsive to bug reports. If I am no longer able to maintain it, I will make reasonable efforts to find a successor maintainer or withdraw the plugin from the directory.
## Repo URL
<!--- Paste a link to your repo here for easy access -->
Link to my plugin:
## Release Checklist
- [ ] I have tested the plugin on
- [ ] Windows
- [ ] macOS
- [ ] Linux
- [ ] Android _(if applicable)_
- [ ] iOS _(if applicable)_
- [ ] My GitHub release contains all required files (as individual files, not just in the source.zip / source.tar.gz)
- [ ] `main.js`
- [ ] `manifest.json`
- [ ] `styles.css` _(optional)_
- [ ] GitHub release name matches the exact version number specified in my manifest.json (_**Note:** Use the exact version number, don't include a prefix `v`_)
- [ ] The `id` in my `manifest.json` matches the `id` in the `community-plugins.json` file.
- [ ] My README.md describes the plugin's purpose and provides clear usage instructions.
- [ ] I have read the developer policies at https://docs.obsidian.md/Developer+policies, and have assessed my plugin's adherence to these policies.
- [ ] I have read the tips in https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines and have self-reviewed my plugin to avoid these common pitfalls.
- [ ] I have added a license in the LICENSE file.
- [ ] My project respects and is compatible with the original license of any code from other plugins that I'm using.
I have given proper attribution to these other projects in my `README.md`.
```

View file

@ -36,6 +36,7 @@ export default tseslint.config(
"versions.json",
"main.js",
".claude/**",
".agents/**",
"coverage/**",
]),
);

View file

@ -260,12 +260,10 @@ export class SyncManager {
let cur = '';
for (let i = 0; i < parts.length - 1; i++) {
cur += (i > 0 ? '/' : '') + parts[i];
if (!this.app.vault.getAbstractFileByPath(cur)) {
try {
await this.app.vault.createFolder(cur);
} catch {
// already exists or failed
}
try {
await this.app.vault.adapter.mkdir(cur);
} catch {
// already exists or failed
}
}
}

View file

@ -138,13 +138,26 @@ export default class GitLabFilesPush extends Plugin {
await this.runAllFiles('pull');
}
private async listAllFilesFromAdapter(dirPath: string): Promise<string[]> {
const results: string[] = [];
try {
const { files, folders } = await this.app.vault.adapter.list(dirPath || '');
results.push(...files);
for (const folder of folders) {
const sub = await this.listAllFilesFromAdapter(folder);
results.push(...sub);
}
} catch { /* ignore inaccessible dirs */ }
return results;
}
private async runAllFiles(op: 'push' | 'pull'): Promise<void> {
const allFiles = this.app.vault.getFiles();
let files = this.filterFilesByVaultFolder(allFiles);
const startPath = this.settings.vaultFolder || '';
const allPaths = await this.listAllFilesFromAdapter(startPath);
await this.gitService.listFiles(this.settings.branch);
await this.gitignoreManager.loadGitignores();
files = files.filter(f => !this.gitignoreManager.isIgnored(this.getNormalizedPath(f.path)));
const files = allPaths.filter(p => !this.gitignoreManager.isIgnored(this.getNormalizedPath(p)));
if (files.length === 0) {
new Notice(`No files to ${op} in the configured vault folder`);

View file

@ -1,8 +1,25 @@
export const BINARY_EXTENSIONS = new Set([
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'pdf', 'zip', 'gz', '7z', 'rar',
'mp3', 'mp4', 'wav', 'ogg', 'webm', 'mov', 'avi', 'wmv', 'webp',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'epub', 'exe', 'dll', 'so',
'ttf', 'woff', 'woff2', 'eot', 'wasm', 'dmg', 'iso'
// Images
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'webp',
'tif', 'tiff', 'heic', 'heif', 'avif', 'jxl',
// Documents
'pdf', 'epub',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'odt', 'ods', 'odp',
// Archives
'zip', 'gz', 'tar', 'bz2', 'xz', 'zst', '7z', 'rar',
// Audio
'mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'opus', 'aiff',
// Video
'mp4', 'webm', 'mov', 'avi', 'wmv', 'mkv', 'flv', 'm4v',
// Fonts
'ttf', 'otf', 'woff', 'woff2', 'eot',
// Executables & binaries
'exe', 'dll', 'so', 'wasm', 'dmg', 'iso', 'apk', 'deb', 'rpm',
// Databases
'sqlite', 'db',
// Design
'psd', 'sketch', 'fig',
]);
export function isBinaryPath(path: string): boolean {