chore: initial project commit

This commit is contained in:
Ignacio Ballesteros
2026-02-17 21:25:46 +01:00
commit 70f7fac23e
34 changed files with 6786 additions and 0 deletions

286
AGENTS.md Normal file
View File

@@ -0,0 +1,286 @@
# AGENTS.md
## Development Environment Commands
### Primary Commands (run inside `nix develop`)
```bash
# Development
nix develop # Enter dev shell with Node.js and tooling
npm run start # Start dev server with hot reload
npm run build # Build production XPI to .scaffold/build/
# Code Quality
npm run lint # Run ESLint on TypeScript source
npm run lint:fix # Auto-fix ESLint issues
npm run format # Format with Prettier
npm run format:check # Check formatting without changes
# Release
npm run release # Bump version, commit, tag, and build
```
### Testing
This project does not currently have automated tests. To test manually:
1. Run `npm run build` to create the XPI
2. Install the XPI in Zotero 7
3. Test export functionality manually via Tools menu
---
## Code Style Guidelines
### Imports and Exports
```typescript
// Use relative imports for internal modules
import { OrgExporter } from "./modules/exporter";
import type { Prefs } from "./modules/prefs";
// Use type-only imports where possible
import type { AddonInfo } from "./addon";
// Use default exports for single main entity
export class OrgExportAnnotationsAddon {}
export function createHooks() {}
// Use named exports for utilities
export async function convertToOrg() {}
export async function testPandoc() {}
```
**Rules:**
- Prefer named exports for functions
- Use `import type` for type-only imports
- Import from `./modules/` with relative paths
- No namespace imports (`import * as X`)
- No `export default` for modules (use named exports)
### Formatting (Prettier)
```json
{
"semi": true, // Always use semicolons
"singleQuote": false, // Use double quotes
"tabWidth": 2, // 2 space indentation
"trailingComma": "es5", // Trailing commas in valid ES5
"printWidth": 100 // Max line length
}
```
### TypeScript Configuration
- **Strict mode enabled**: All type checking rules active
- **Target**: ES2022 with DOM types
- **Module**: ESNext with bundler resolution
- **Source maps**: Enabled for debugging
- **Output**: Bundled to `.scaffold/build/addon/content/scripts/`
### Naming Conventions
| Entity | Convention | Examples |
| --------------- | ---------------- | ---------------------------------------------------------- |
| Classes | PascalCase | `OrgExporter`, `Prefs`, `AddonInfo` |
| Functions | camelCase | `convertToOrg`, `isAnnotationNote`, `ensureNotesDirectory` |
| Methods | camelCase | `init()`, `exportAttachment()`, `cleanupTempFile()` |
| Private Methods | camelCase | Prefix with `private` keyword |
| Constants | UPPER_SNAKE_CASE | `PREF_PREFIX`, `MENU_IDS` |
| Interfaces | PascalCase | `AddonInfo`, `Hooks` |
| Types | PascalCase | `export type Preference = ...` |
| File Names | kebab-case | `converter.ts`, `exporter.ts`, `preferences.js` |
```typescript
// Constants
const PREF_PREFIX = "extensions.zotero.orgexportannotations";
const MENU_IDS = { toolsExportAll: "...", itemExportSelected: "..." };
// Private methods
private async getLastAnnotationsNote(): Promise<Zotero.Item | undefined> {}
private isAnnotationNote(noteHTML: string): boolean {}
```
### Type Annotations
```typescript
// Explicit return types on functions
export async function convertToOrg(...): Promise<void> {}
export function testPandoc(pandocPath: string): Promise<boolean> {}
// Type assertions for Zotero API returns
const title = attachment.parentItem.getField("title") as string;
const citationKey = item.getField("citationKey") as string;
// Optional chaining with nullish coalescing
const citationKey = note.parentItem?.getField("citationKey") || "temp";
// Generic type parameters
private getPref<T>(key: string, defaultValue: T): T {
return (value as T) ?? defaultValue;
}
```
### Error Handling
```typescript
// Standard pattern with logging
try {
await operation();
Zotero.OrgExportAnnotations.log("Operation succeeded");
} catch (error) {
Zotero.OrgExportAnnotations.error("Operation failed", error as Error);
// Optionally re-throw
throw error;
}
// Empty catch blocks must use underscore prefix
try {
await cleanup();
} catch (_error) {
// Cleanup errors are non-critical
}
// Return error values instead of throwing for utility functions
export async function testPandoc(pandocPath: string): Promise<boolean> {
try {
await Zotero.Utilities.Internal.exec(pandocPath, ["--version"]);
return true;
} catch {
return false;
}
}
```
### Logging
```typescript
// Debug logging (respects debug mode)
Zotero.OrgExportAnnotations.log("Message", ...args);
// Error logging (always logged)
Zotero.OrgExportAnnotations.error("Message", error as Error);
// Warning logging (always logged)
Zotero.OrgExportAnnotations.warn("Message");
// Verbose debugging with objects
Zotero.OrgExportAnnotations.log("Processing item", { id, title });
```
### Async/Await Patterns
```typescript
// Use for...of for sequential async operations
for (const attachment of attachments) {
if (await this.exportAttachment(attachment)) {
count++;
}
}
// Early returns with await
if (!item.isFileAttachment()) return false;
if (!path) throw new Error("Notes path not configured");
```
### Zotero API Usage
```typescript
// Non-null assertions for known APIs
Zotero.BetterNotes!.api.convert.note2md(note, notesPath);
// Zotero utilities for file operations
await Zotero.File.putContentsAsync(path, content);
const content = await Zotero.File.getContentsAsync(path);
await Zotero.Utilities.Internal.exec(command, args);
// Zotero preferences
Zotero.Prefs.get(key, global);
Zotero.Prefs.set(key, value, global);
// Zotero items
const item = Zotero.Items.get(id);
await Zotero.Items.trashTx(id);
```
### Class Structure
```typescript
export class MyClass {
// Public properties
public property!: string;
// Private properties
private internal = { enabled: false };
// Constructor with dependency injection
constructor(private deps: Dependencies) {}
// Public methods
public async doSomething(): Promise<void> {
try {
// Implementation
} catch (error) {
this.handleError(error as Error);
}
}
// Private helper methods
private handleError(error: Error): void {
Zotero.OrgExportAnnotations.error("Error occurred", error);
}
}
```
### ESLint Rules
- **Unused variables**: Error, but ignored if prefixed with `_`
- **Explicit any**: Warning (prefer specific types)
- **No-empty**: Error for empty catch blocks (use `catch (_error)`)
### File Organization
```
src/
├── index.ts # Main entry point
├── addon.ts # Core addon class
├── hooks.ts # Lifecycle hooks
└── modules/
├── prefs.ts # Preferences management
├── exporter.ts # Core export logic
├── converter.ts # Format conversion
├── notifier.ts # Zotero event handlers
└── menu.ts # UI menu registration
```
### Common Patterns
**Preference getters/setters**:
```typescript
get notesPath(): string {
const path = this.getPref("notesPath", "");
if (path && path.startsWith("~")) {
return path.replace("~", PathUtils.profileDir);
}
return path;
}
set notesPath(value: string) {
this.setPref("notesPath", value);
}
```
**Filter operations**:
```typescript
return items
.filter((item) => this.checkCondition(item))
.filter((item) => item.parentItem !== undefined);
```
**Reduce operations**:
```typescript
return notes.reduce((max, current) => (current.dateModified > max.dateModified ? current : max));
```