Skip to content

Development Guide

This guide provides comprehensive information for developers working on the Warcraft II Notifications Plugin. It covers setup, development workflows, testing strategies, and contribution guidelines.

Recommended extensions:

  • ESLint: For linting
  • Prettier: For code formatting
  • TypeScript: For type checking
  • Bun for Visual Studio Code: For Bun support

Settings (.vscode/settings.json):

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"typescript.tsdk": "node_modules/typescript/lib"
}

Create a .env file for local development:

Terminal window
# Debug mode
DEBUG_OPENCODE=1
# Custom data directory (optional)
SOUNDS_DATA_DIR=/path/to/custom/sounds
# Custom base URL (optional, legacy)
SOUNDS_BASE_URL=https://custom-cdn.com/sounds

opencode-warcraft-notifications/
├── .github/ # GitHub workflows and automation
│ ├── scripts/ # Workflow helper scripts
│ └── workflows/ # CI/CD workflows
├── data/ # Bundled sound files
│ ├── alliance/ # Alliance faction sounds
│ └── horde/ # Horde faction sounds
├── docs/ # Documentation
│ ├── github-workflows/ # Workflow documentation
│ └── schemas/ # JSON schemas
├── src/ # Source code
│ ├── sound-data/ # Sound metadata
│ │ ├── alliance.ts # Alliance sound entries
│ │ ├── horde.ts # Horde sound entries
│ │ ├── index.ts # Sound data exports
│ │ └── types.ts # Type definitions
│ ├── bundled-sounds.ts # Bundled sound management
│ ├── notification.ts # Main plugin logic
│ ├── plugin-config.ts # Configuration management
│ ├── sounds.ts # Sound selection and paths
│ └── test-utils.ts # Testing utilities
├── typings/ # TypeScript type definitions
├── index.ts # Plugin entry point
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
├── tsconfig.test.json # Test TypeScript configuration
├── eslint.config.cjs # ESLint configuration
└── .prettierrc # Prettier configuration

Contains all source code:

  • Core modules: notification.ts, plugin-config.ts, sounds.ts, bundled-sounds.ts, schema-validator.ts
  • Sound data: sound-data/ directory with faction-specific sound entries
  • Tests: *.test.ts files for unit and integration tests
  • Utilities: test-utils.ts for testing helpers

Contains bundled WAV files:

  • alliance/: 50+ Alliance unit sounds
  • horde/: 50+ Horde unit sounds

Documentation files:

  • API documentation: API reference
  • Architecture: System design and component diagrams
  • Workflows: CI/CD pipeline documentation
  • Schemas: JSON schema definitions

The project includes a comprehensive documentation site built with Astro and Starlight, located in the pages/ directory. The documentation is automatically deployed to GitHub Pages on every push to the main branch.

The documentation site requires several generation steps before the Astro build:

Terminal window
# Full build process (from pages directory)
cd pages
bun run build
# Development mode with hot reload
bun run dev

The build process follows this sequence:

  1. Generate Favicon (bun run generate-favicon)

    • Dynamically generates pages/public/favicon.svg
    • Uses the blocky text utility with “W” character
    • Matches the Warcraft-themed aesthetic of the logos
    • Generated file is gitignored and created on each build
  2. Transform Documentation (bun run transform)

    • Copies files from docs/ to pages/src/content/docs/
    • Converts markdown to Astro-compatible format
  3. Build Astro Site (astro build)

    • Builds the static site to pages/dist/
  4. Fix Links (bun run fix-links)

    • Updates internal links for GitHub Pages deployment

The favicon is automatically generated during the build process, but you can regenerate it manually:

Terminal window
cd pages
bun run generate-favicon

The favicon generation script (pages/generate-favicon.mjs):

  • Creates a blocky “W” character using the same style as the project logos
  • Outputs to pages/public/favicon.svg
  • Uses dark theme with 8px block size for optimal rendering
  • The generated file is gitignored and must be regenerated for each build

Configuration:

The favicon can be customized by modifying pages/generate-favicon.mjs:

const faviconSVG = blockyTextToSVG('W', {
theme: 'dark', // 'light' or 'dark'
blockSize: 8, // Size of each pixel block
charSpacing: 0, // Spacing between characters
optimize: true, // Enable path optimization
});
  1. Edit source documentation in the docs/ directory
  2. Run development server:
    Terminal window
    cd pages
    bun run dev
  3. View changes at http://localhost:4321
  4. Build for production:
    Terminal window
    bun run build

All documentation scripts are run from the pages/ directory:

{
"generate-favicon": "bun generate-favicon.mjs",
"transform": "node transform-docs.js",
"fix-links": "node fix-links.js",
"test-links": "node test-links.js",
"dev": "bun run generate-favicon && bun run transform && astro dev",
"build": "bun run generate-favicon && bun run transform && astro build && bun run fix-links",
"preview": "astro preview",
"verify": "bun run test-links"
}

The plugin implements runtime validation of plugin.json configuration using Zod schemas. Configuration is validated automatically when the plugin loads, ensuring that any configuration errors are caught early with clear, actionable error messages.

The validation schema is defined in src/schema-validator.ts and enforces:

  • faction: Must be one of 'alliance', 'horde', or 'both' (optional)
  • soundsDir: Must be a string (optional)
  • No extra keys: Unknown configuration keys are rejected
// Valid: Only alliance sounds
{ faction: 'alliance' }
// Valid: Custom sounds directory
{ soundsDir: '/custom/path/to/sounds' }
// Valid: Both settings
{ faction: 'horde', soundsDir: '~/.cache/sounds' }
// Valid: Empty (uses defaults)
{}
// Invalid: Unknown faction
{ faction: 'night-elf' }
// Error: faction: Invalid enum value. Must be one of: 'alliance', 'horde', 'both'
// Invalid: Wrong type for soundsDir
{ soundsDir: 123 }
// Error: soundsDir: Expected string, received undefined
// Invalid: Unrecognized keys
{ faction: 'alliance', unknownKey: 'value' }
// Error: Unrecognized configuration key(s): unknownKey. Only 'soundsDir' and 'faction' are allowed.

Validation errors provide specific, actionable feedback:

[Warcraft Notifications] Configuration validation failed:
- faction: Invalid enum value. Must be one of: 'alliance', 'horde', 'both'
- soundsDir: Expected string, received undefined
Configuration file: /path/to/.opencode/plugin.json

When writing tests that involve configuration:

import { validatePluginConfig } from './schema-validator';
describe('My Feature', () => {
it('should handle valid config', () => {
const result = validatePluginConfig({ faction: 'alliance' });
expect(result.valid).toBe(true);
});
it('should reject invalid config', () => {
const result = validatePluginConfig({ faction: 'invalid' });
expect(result.valid).toBe(false);
expect(result.errors).toBeDefined();
});
});

If you encounter validation errors during development:

  1. Check the error message: It will specify exactly which field is invalid and why
  2. Verify the schema: Look at docs/schemas/plugin.json.schema for the expected structure
  3. Enable debug mode: Set DEBUG_OPENCODE=1 to see validation warnings
  4. Test your config: Use validatePluginConfig() in tests to verify expected behavior

Terminal window
git checkout -b feature/your-feature-name

Edit source files in src/ directory:

// Example: Adding a new sound category
export const newUnitSounds = {
newUnitSelected: ['new_unit_selected1.wav', 'new_unit_selected2.wav'],
newUnitAcknowledge: ['new_unit_acknowledge1.wav'],
};
Terminal window
# Run all tests
bun run test
# Run tests in watch mode
bun run test:watch
# Run tests with coverage
bun run test:coverage
# Run verbose tests
bun run test:verbose
Terminal window
bun run type-check
Terminal window
# Check for linting errors
bun run lint
# Auto-fix linting errors
bun run lint --fix
Terminal window
# Format all files
bun run format
# Check formatting without changes
bun run format:check

Use conventional commit messages:

Terminal window
git add .
git commit -m "feat: add new unit sound category"

Commit Message Format:

  • feat: - New feature
  • fix: - Bug fix
  • docs: - Documentation changes
  • style: - Code style changes (formatting, etc.)
  • refactor: - Code refactoring
  • test: - Adding or updating tests
  • chore: - Maintenance tasks
Terminal window
git push origin feature/your-feature-name

Then create a Pull Request on GitHub.


Test individual functions in isolation:

import { describe, test, expect } from 'bun:test';
import { determineSoundFaction } from './sounds';
describe('determineSoundFaction', () => {
test('should identify Alliance sounds', () => {
expect(determineSoundFaction('human_selected1.wav')).toBe('alliance');
expect(determineSoundFaction('knight_acknowledge1.wav')).toBe('alliance');
});
test('should identify Horde sounds', () => {
expect(determineSoundFaction('orc_selected1.wav')).toBe('horde');
expect(determineSoundFaction('death_knight_acknowledge1.wav')).toBe('horde');
});
});

Test component interactions:

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { loadPluginConfig } from './plugin-config';
import { createTempDir, cleanupTempDir } from './test-utils';
describe('Plugin Configuration Integration', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await createTempDir();
});
afterEach(async () => {
await cleanupTempDir(tempDir);
});
test('should load project-specific configuration', async () => {
// Create test config
const configPath = join(tempDir, '.opencode', 'plugin.json');
await mkdir(dirname(configPath), { recursive: true });
await writeFile(
configPath,
JSON.stringify({
'@pantheon-ai/opencode-warcraft-notifications': {
faction: 'horde',
},
}),
);
// Load config
const config = await loadPluginConfig('@pantheon-ai/opencode-warcraft-notifications');
expect(config.faction).toBe('horde');
});
});

Test boundary conditions:

describe('Edge Cases', () => {
test('should handle empty sound directory', async () => {
const sounds = await getSoundsByFaction('alliance');
expect(sounds).toBeArray();
expect(sounds.length).toBeGreaterThan(0);
});
test('should handle missing configuration file', async () => {
const config = await loadPluginConfig('nonexistent-plugin');
expect(config).toEqual({});
});
});

Test error handling:

describe('Error Handling', () => {
test('should handle invalid faction', () => {
expect(() => {
// @ts-expect-error Testing invalid input
getSoundsByFaction('invalid');
}).toThrow();
});
test('should handle missing sound file gracefully', async () => {
const exists = await soundExists('nonexistent.wav', 'alliance');
expect(exists).toBe(false);
});
});

Use test-utils.ts for common testing patterns:

import { createMockContext, createTempDir, cleanupTempDir } from './test-utils';
// Create mock OpenCode context
const ctx = createMockContext();
// Create temporary directory for tests
const tempDir = await createTempDir();
// Cleanup after tests
await cleanupTempDir(tempDir);
Terminal window
# Run all tests
bun test
# Run specific test file
bun test src/sounds.test.ts
# Run tests matching pattern
bun test --test-name-pattern "faction"
# Run tests with coverage
bun test --coverage
# Run tests in watch mode
bun test --watch
  • Overall coverage: > 80%
  • Critical paths: > 95%
  • Edge cases: > 70%

The project uses ESLint with TypeScript support:

Terminal window
# Check for linting errors
bun run lint
# Auto-fix linting errors
bun run lint --fix

ESLint Configuration (eslint.config.cjs):

  • TypeScript rules
  • Import/export rules
  • JSDoc rules
  • Code quality rules (SonarJS)
  • Prettier integration

The project uses Prettier for code formatting:

Terminal window
# Format all files
bun run format
# Check formatting
bun run format:check

Prettier Configuration (.prettierrc):

{
"semi": true,
"trailingComma": "all",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}

TypeScript is used for type safety:

Terminal window
# Type check without emitting files
bun run type-check
# Build TypeScript
bun run build
# Watch mode
bun run dev

TypeScript Configuration (tsconfig.json):

  • Strict mode enabled
  • ES2022 target
  • Module resolution: bundler
  • Path aliases supported

Enable debug logging:

Terminal window
DEBUG_OPENCODE=1 bun test

Debug Output:

  • Configuration loading attempts
  • Sound installation progress
  • File operation results
  • Error details

Use Bun’s built-in debugger:

Terminal window
# Run tests with debugger
bun --inspect test
# Run specific test with debugger
bun --inspect test src/sounds.test.ts
  1. Enable debug mode:

    Terminal window
    DEBUG_OPENCODE=1 opencode
  2. Check logs:

    • Configuration loading
    • Sound installation
    • Event handling
    • Sound playback
  3. Verify sound files:

    Terminal window
    # Check default data directory
    ls -la ~/.local/share/opencode/storage/plugin/@pantheon-ai/opencode-warcraft-notifications/sounds/
    # Check alliance sounds
    ls -la ~/.local/share/opencode/storage/plugin/@pantheon-ai/opencode-warcraft-notifications/sounds/alliance/
    # Check horde sounds
    ls -la ~/.local/share/opencode/storage/plugin/@pantheon-ai/opencode-warcraft-notifications/sounds/horde/
Terminal window
# Check project config
cat .opencode/plugin.json
# Check global config
cat ~/.config/opencode/plugin.json
# Verify JSON syntax
bun run validate:schema

  • Tests pass locally
  • Code follows style guide
  • Types are properly defined
  • Documentation is updated
  • Commit messages follow convention
  • No console.log statements (use DEBUG_OPENCODE)
  • Error handling is appropriate
  • Performance impact is minimal
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing performed
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] Tests pass locally
- [ ] No new warnings

The workflow automatically determines version bumps:

  • MAJOR: Breaking changes, API changes
  • MINOR: New features, backwards-compatible additions
  • PATCH: Bug fixes, documentation, small improvements

If needed, trigger a manual release:

Terminal window
# Trigger workflow with specific version type
gh workflow run smart-version-bump.yml -f version_type=minor
  • All tests pass
  • Documentation is up-to-date
  • CHANGELOG is updated (automated)
  • Version bump is appropriate
  • No breaking changes (unless MAJOR)

  • Keep functions small and focused
  • Use descriptive variable names
  • Group related functionality
  • Avoid deep nesting
// Good: Graceful error handling
try {
await installBundledSoundsIfMissing(dataDir);
} catch (err) {
if (process.env.DEBUG_OPENCODE) {
console.warn('Installation failed:', err);
}
// Continue with existing sounds
}
// Bad: Unhandled errors
await installBundledSoundsIfMissing(dataDir); // May throw
// Good: Explicit types
const getSoundPath = (
filename: string,
faction: 'alliance' | 'horde',
dataDir?: string,
): string => {
// ...
};
// Bad: Implicit any
const getSoundPath = (filename, faction, dataDir) => {
// ...
};
// Good: Descriptive test names
test('should return alliance for human unit sounds', () => {
expect(determineSoundFaction('human_selected1.wav')).toBe('alliance');
});
// Bad: Vague test names
test('faction test', () => {
expect(determineSoundFaction('human_selected1.wav')).toBe('alliance');
});
/**
* Get the full path to a sound file in the faction-specific subdirectory.
*
* @param filename - Sound filename (e.g., 'human_selected1.wav')
* @param faction - Faction the sound belongs to
* @param dataDir - Optional override data directory
* @returns Absolute path to the sound file
*
* @example
* ```typescript
* const path = getSoundPath('human_selected1.wav', 'alliance');
* // Returns: '/home/user/.local/share/.../alliance/human_selected1.wav'
* ```
*/
export const getSoundPath = (
filename: string,
faction: 'alliance' | 'horde',
dataDir?: string,
): string => {
// ...
};



Document Version: 1.0
Last Updated: 2025-11-10
Maintained By: Pantheon AI Team