chore: initial project commit
This commit is contained in:
286
AGENTS.md
Normal file
286
AGENTS.md
Normal 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));
|
||||
```
|
||||
Reference in New Issue
Block a user