7.8 KiB
7.8 KiB
AGENTS.md
Development Environment Commands
Primary Commands (run inside nix develop)
# 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:
- Run
npm run buildto create the XPI - Install the XPI in Zotero 7
- Test export functionality manually via Tools menu
Code Style Guidelines
Imports and Exports
// 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 typefor type-only imports - Import from
./modules/with relative paths - No namespace imports (
import * as X) - No
export defaultfor modules (use named exports)
Formatting (Prettier)
{
"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 |
// 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
// 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
// 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
// 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
// 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
// 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
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:
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:
return items
.filter((item) => this.checkCondition(item))
.filter((item) => item.parentItem !== undefined);
Reduce operations:
return notes.reduce((max, current) => (current.dateModified > max.dateModified ? current : max));