287 lines
7.8 KiB
Markdown
287 lines
7.8 KiB
Markdown
# 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));
|
|
```
|