Thanks for your interest in contributing to mdite! This document provides guidelines for contributing to the project.
mdite is a markdown documentation toolkit that treats documentation as a connected system. We're building features that enable system-wide operations: validation, dependency analysis, search, and output.
Be respectful, inclusive, and constructive. We're all here to build something great together.
- Node.js >= 20.0.0
- npm
- Git
# Clone the repository
git clone https://github.com/radleta/mdite.git
cd mdite
# Install dependencies
npm install
# Build the project
npm run build
# Link for local testing
npm link
# Test the CLI
mdite --version
# Run smoke tests against examples
cd examples
./run-all-examples.shThis project uses Husky and lint-staged for automated pre-commit quality checks.
Hooks are automatically configured when you run npm install (via the prepare script).
Husky installs the hooks to .husky/ directory, and lint-staged runs linting/formatting only on staged files for fast performance.
The pre-commit hook (.husky/pre-commit):
- Prevents committing
scratch/,claude-iterate/,coverage/, ornode_modules/directories - Runs
lint-stagedwhich automatically:- Runs ESLint with
--fixon staged.tsfiles - Runs Prettier with
--writeon staged TypeScript, JSON, Markdown, and YAML files - Re-stages the fixed files
- Runs ESLint with
What is lint-staged?
lint-staged only runs linters on files you've staged for commit, making pre-commit checks extremely fast. Configuration is in package.json under the lint-staged key.
While not enforced by a hook, we follow Conventional Commits format:
type(scope): description
[optional body]
[optional footer]
Valid types:
feat:- New featurefix:- Bug fixdocs:- Documentationstyle:- Formattingrefactor:- Code restructuringtest:- Testschore:- Build/toolingperf:- Performance
In exceptional cases, you can bypass hooks:
git commit --no-verifyNote: This is not recommended and should only be used when absolutely necessary.
# Run all tests
npm test
# Watch mode for development
npm run test:watch
# Coverage report
npm run test:coverage
# Run specific test files
npm run test:unit
npm run test:integration
# Run smoke tests (examples)
npm run examples
# or
cd examples && ./run-all-examples.sh
# Test specific example
cd examples/01-valid-docs && mdite lint- Unit Tests: Isolated module testing (
tests/unit/) - Integration Tests: Full CLI workflows (
tests/integration/) - Smoke Tests: Manual verification (
examples/) ⭐ NEW
# Lint code
npm run lint
# Type check
npm run typecheck
# Format code
npm run format
# Run all checks (lint + typecheck + test)
npm run validateThis project uses manual dependency management (no Dependabot). Dependencies are updated in batches by maintainers. npm audit runs in CI/CD and fails on high/critical vulnerabilities. If you discover a vulnerability, open an issue.
Fork the repository on GitHub, then clone your fork:
git clone https://github.com/YOUR_USERNAME/mdite.git
cd mdite
git remote add upstream https://github.com/radleta/mdite.gitCreate a feature branch from main:
git checkout main
git pull upstream main
git checkout -b feature/my-awesome-featureUse descriptive branch names:
feature/- New featuresfix/- Bug fixesdocs/- Documentation updatesrefactor/- Code refactoringtest/- Test improvements
- Write clean, readable code
- Follow existing code style and conventions
- Add tests for new functionality
- Update documentation as needed
- Keep commits focused and atomic
# Run validation suite
npm run validate
# Run smoke tests
npm run examples
# Test the CLI locally
npm run build
npm link
mdite --version
mdite initFollow Conventional Commits format:
type(scope): brief description
Longer description if needed.
- Bullet points for details
- More information
Closes #123
Types:
feat:- New featurefix:- Bug fixdocs:- Documentation changesrefactor:- Code refactoring (no functional changes)test:- Test additions or changeschore:- Build process, tooling, dependenciesperf:- Performance improvementsstyle:- Code style changes (formatting, etc.)
Examples:
git commit -m "feat: add external link validation"
git commit -m "fix: handle missing frontmatter gracefully
- Check if frontmatter exists before parsing
- Return empty object if missing
- Add tests for edge case
Closes #45"
git commit -m "docs: add examples for custom rules"# Push to your fork
git push origin feature/my-awesome-feature
# Create PR on GitHubPR Guidelines:
- Fill out the PR template completely
- Reference related issues
- Ensure all CI checks pass
- Respond to review feedback promptly
Before submitting:
- ✅ All automated tests pass (
npm test) - ✅ Code is linted and formatted (
npm run validate) - ✅ Smoke tests pass (
npm run examples) ⭐ NEW - ✅ Documentation is updated
- ✅ Commit messages follow conventional commits
mdite/
├── src/
│ ├── commands/ # CLI command implementations (lint, deps, init, config)
│ │ # Future: query, cat, toc
│ ├── core/ # Business logic (graph analysis, validation, etc.)
│ │ ├── doc-linter.ts # Main orchestrator
│ │ ├── graph-analyzer.ts # Graph foundation (enables all features)
│ │ ├── link-validator.ts # Link/anchor validation
│ │ └── ...
│ ├── types/ # Zod schemas and TypeScript types
│ ├── utils/ # Utilities (logger, fs, errors)
│ ├── cli.ts # CLI setup (Commander.js)
│ └── index.ts # Entry point
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ ├── fixtures/ # Test fixtures
│ └── setup.ts # Test configuration
├── examples/ # Runnable examples & smoke tests
└── dist/ # Compiled output (generated)
Key files:
src/cli.ts- Register new commands heresrc/core/graph-analyzer.ts- Graph foundation for all featuressrc/types/*.ts- Zod schemas for validationtests/fixtures/- Sample docs for automated testingexamples/- Runnable examples for manual testing & documentation
- Create command file:
src/commands/my-command.ts - Export command function that returns a Commander
Command - Register in
src/cli.ts - Add tests in
tests/unit/ortests/integration/ - Update README.md with command documentation
Example:
// src/commands/my-command.ts
import { Command } from 'commander';
import { Logger } from '../utils/logger.js';
export function myCommand(): Command {
return new Command('my-command')
.description('Do something awesome')
.argument('<path>', 'Path argument')
.option('-f, --force', 'Force flag')
.action(async (path, options) => {
const logger = new Logger();
logger.info(`Running my-command for ${path}`);
// Implementation...
});
}// src/cli.ts
import { myCommand } from './commands/my-command.js';
// In cli() function:
program.addCommand(myCommand());- Define rule logic in appropriate module
- Add to rule configuration in types
- Update documentation
- Add comprehensive tests
mdite follows Unix CLI best practices. When adding or modifying commands, follow the hybrid approach:
- Shared sections (identical across commands) →
src/utils/help-text.ts - Command-specific sections (unique content) → Colocate with command
Pattern: Define help as constants at the top of command files (DESCRIPTION, EXAMPLES, SEE_ALSO), then add via .addHelpText('after', CONSTANT).
Reference implementations:
src/commands/config.ts- Simple (~35 lines)src/commands/init.ts- Medium (~40 lines)src/commands/files.ts- Complex (~90 lines, Unix composition focus)
Testing: Add integration tests to tests/integration/cli-help.test.ts checking for presence of each section
- All new features need tests
- Tests should be fast (<10 seconds for full suite)
- Tests should be deterministic (no flaky tests)
- Use test fixtures for complex scenarios
- Mock external dependencies when appropriate
- Test stdout/stderr separation for commands that produce output
Test structure:
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('MyFeature', () => {
beforeEach(() => {
// Setup
});
afterEach(() => {
// Cleanup
});
it('should do something', () => {
// Arrange
const input = 'test';
// Act
const result = doSomething(input);
// Assert
expect(result).toBe('expected');
});
});Testing Unix CLI Features:
When testing CLI commands, properly test stdout/stderr separation:
// Integration test pattern
function runCli(args: string[]): {
stdout: string;
stderr: string;
exitCode: number;
} {
const { spawnSync } = require('child_process');
const result = spawnSync('node', ['dist/src/index.js', ...args], {
cwd: testDir,
encoding: 'utf-8',
});
return {
stdout: result.stdout || '',
stderr: result.stderr || '',
exitCode: result.status || 0,
};
}
// Test stdout/stderr separation
it('should output data to stdout and messages to stderr', () => {
const result = runCli(['lint', '--format', 'json']);
// Data goes to stdout
expect(result.stdout).toContain('{');
const json = JSON.parse(result.stdout);
// Messages go to stderr (or are suppressed in quiet mode)
expect(result.stderr).toContain('Linting');
// Exit codes
expect(result.exitCode).toBe(1); // validation error
});
// Test quiet mode
it('should suppress messages in quiet mode', () => {
const result = runCli(['lint', '--quiet']);
// Stderr should not have info messages in quiet mode
expect(result.stderr).not.toContain('Building dependency graph');
// But errors still appear
if (result.exitCode !== 0) {
expect(result.stdout).toBeTruthy();
}
});Unit test pattern for Logger:
import { vi, type MockInstance } from 'vitest';
describe('Logger', () => {
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
beforeEach(() => {
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
it('should output data to stdout', () => {
logger.log('data');
expect(consoleLogSpy).toHaveBeenCalledWith('data');
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should output messages to stderr', () => {
logger.info('info message');
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('info message'));
expect(consoleLogSpy).not.toHaveBeenCalled();
});
});When adding a new feature, consider adding an example to demonstrate it.
Add an example if:
- Feature is user-facing
- Feature demonstrates a common use case
- Feature is complex and benefits from demonstration
- Feature adds a new configuration option
-
Choose directory in appropriate phase:
- Phase 1 (
01-04): Core features (orphans, links, anchors) - Phase 2 (
05-06): Real-world scenarios, config variations - Phase 3 (
07): Edge cases (cycles, deep nesting, etc.)
- Phase 1 (
-
Create files:
README.md- Explain the example clearly- Markdown files demonstrating the feature
- Config file (
.mditerc,mdite.config.js, etc.)
-
Update
examples/run-all-examples.sh:run_example \ "example-name" \ "path/to/example" \ false \ # or true if errors expected "Brief description" -
Update
examples/README.mdwith the new example -
Test:
cd examples/your-example && mdite lint cd .. && ./run-all-examples.sh
See examples/README.md for existing examples and detailed documentation.
mdite follows industry best practices from established CLI tools (git, docker, npm, ripgrep) to provide comprehensive, agent-optimized help text. This section documents patterns for maintaining and extending CLI help.
Shared Content (src/utils/help-text.ts):
- Content identical across all commands
- Examples: EXIT_CODES, ENVIRONMENT_VARS, CONFIG_PRECEDENCE
- Export as constants, import where needed
Command-Specific Content (colocated with command files):
- DESCRIPTION, EXAMPLES, OUTPUT, SEE_ALSO constants
- Defined at top of command file (e.g.,
src/commands/lint.ts) - Registered via
.addHelpText('after', CONSTANT)
Every command should include these sections in order:
const DESCRIPTION = `
DESCRIPTION:
Brief explanation of what command does (1-2 sentences).
Use cases:
- Use case 1
- Use case 2
- Use case 3
Key features or behaviors:
- Feature 1
- Feature 2
`;Guidelines:
- Start with high-level purpose
- Include 3-4 use cases showing when to use the command
- List key features or special behaviors
- Keep concise but informative
const EXAMPLES = `
EXAMPLES:
Basic usage:
$ mdite command
With common option:
$ mdite command --option value
Advanced scenario:
$ mdite command --flag1 --flag2 value
Piping to other tools:
$ mdite command | grep "pattern"
JSON output for tooling:
$ mdite command --format json | jq '.field'
CI/CD integration:
$ mdite command --quiet || exit 1
Complex workflow:
$ mdite command --multiple --flags | xargs other-tool
Edge case:
$ mdite command --special-case
`;Guidelines:
- Aim for 8-10 examples covering all use cases
- Use 6-space indentation for consistency
- Prefix commands with
$for clarity - Add blank line between examples
- Include: basic usage, common options, advanced scenarios, piping, CI/CD, edge cases
- Show Unix tool composition (grep, jq, xargs, etc.)
const OUTPUT = `
OUTPUT:
- Data to stdout (pipeable): What data goes to stdout
- Messages to stderr (suppressible): What messages go to stderr
- Quiet mode (--quiet): Suppresses stderr, keeps stdout data
- Format options (--format):
• format1: Description of format
• format2: Description of format
• format3: Description of format
- Color handling: Auto-disabled for JSON and when piped, respects NO_COLOR/FORCE_COLOR
- Exit codes: 0=success, 1=validation errors, 2=invalid arguments, 130=interrupted
- TTY detection: Colors auto-disable when piped to other tools
- Error output: Where errors appear (stdout vs stderr)
- Pipe-friendly: Works with grep, jq, awk - clean stdout for processing
- Additional behavior: Any other relevant output behavior
`;Guidelines:
- Aim for 8-10 bullet points covering all output behaviors
- Document stdout/stderr separation (critical for Unix composition)
- Explain --quiet mode behavior
- List all --format options with descriptions
- Document color handling (TTY detection, NO_COLOR, FORCE_COLOR)
- List exit codes
- Explain piping behavior
- Mention tools that work well with the command
const SEE_ALSO = `
SEE ALSO:
Core workflow:
mdite lint Validate documentation structure
mdite deps Analyze file dependencies
Configuration:
mdite config View current configuration
mdite init Create config file
Global:
mdite --help Main help with exit codes and environment variables
`;Guidelines:
- Organize by tier (Core workflow, Configuration, Global)
- Include 3-5 references per command
- Brief description per reference (2-5 words)
- Bidirectional links (lint ↔ deps, init ↔ config)
- All commands reference global help
Tier Structure:
- Core workflow: lint → deps → files → cat (main feature workflow)
- Configuration: init ↔ config (configuration management)
- Global: All commands reference
mdite --help
export function myCommand(): Command {
return new Command('my-command')
.description('Brief one-line description')
.addHelpText('after', DESCRIPTION)
.argument('<arg>', 'Argument description')
.option('--flag', 'Flag description')
.addHelpText('after', EXAMPLES)
.addHelpText('after', OUTPUT)
.addHelpText('after', SEE_ALSO)
.action(async (arg, options, command) => {
// Implementation
});
}Registration order:
- DESCRIPTION (after command definition)
- Arguments and options
- EXAMPLES (after options)
- OUTPUT (after examples)
- SEE_ALSO (after output)
- Indentation: 6 spaces for examples, 4 spaces for bullet sub-items
- Command prefix:
$before all command examples - Blank lines: Between examples, between tier groups in SEE_ALSO
- Bullet format:
-for main bullets,•for sub-items (format options) - Line length: Aim for 80 characters, wrap longer lines
- Escape sequences: Support
\n,\tin examples (e.g., --separator)
When adding or updating command help, verify:
- DESCRIPTION constant present (1-2 sentences + use cases + features)
- EXAMPLES constant present (8-10 examples covering all use cases)
- OUTPUT constant present (8-10 bullets covering all output behaviors)
- SEE_ALSO constant present (3-5 references organized by tier)
- All constants registered via
.addHelpText('after', CONSTANT) - Registration order correct (DESCRIPTION → args/opts → EXAMPLES → OUTPUT → SEE_ALSO)
- Formatting consistent (6-space indent, $ prefix, blank lines)
- Cross-references bidirectional (if command references another, that command references back)
- Manual test:
mdite [command] --helpdisplays correctly - Integration test added to
tests/integration/cli-help.test.ts
Simple: src/commands/init.ts (~80 lines)
Medium: src/commands/config.ts (~90 lines)
Complex: src/commands/files.ts (~120 lines)
- Industry patterns:
scratch/cli-help-improvements/patterns.md- Analysis of git, docker, npm, ripgrep - Architecture:
ARCHITECTURE.md- Unix CLI Integration Patterns section - User docs:
README.md- Commands section (keep aligned with help text)
- Update README.md for user-facing features
- Add JSDoc comments for public APIs
- Update examples as needed
- Keep documentation in sync with code
Note: This section is for maintainers with publish rights.
- Maintainer access to repository
- npm account with publish rights to
mditepackage NPM_TOKENconfigured in GitHub repository secrets
git checkout main
git pull origin main
npm run validate # All tests must passBefore versioning, ensure CHANGELOG.md is up-to-date:
- Move items from
[Unreleased]section to the new version section - Update version header with release date
- Review that all notable changes are documented
Use npm version command to bump the version following Semantic Versioning:
# For bug fixes (0.1.0 → 0.1.1)
npm version patch
# For new features (0.1.0 → 0.2.0)
npm version minor
# For breaking changes (0.1.0 → 1.0.0)
npm version majorThis command will:
- Run
npm run validateto ensure code quality (preversion hook) - Update version in package.json
- Update CHANGELOG.md with the new version (version hook)
- Create a git commit with the version change
- Create a git tag (e.g.,
v0.1.1) - Push changes and tags to remote (postversion hook)
Once the tag is pushed, GitHub Actions automatically:
- Detects the tag push
- Runs all tests
- Builds the project
- Publishes to npm with provenance attestation
- Creates a GitHub release with CHANGELOG link
After the GitHub Action completes:
# Check npm package
open https://www.npmjs.com/package/mdite
# Check GitHub release
open https://github.com/radleta/mdite/releases
# Test installation
npm install -g mdite@latest
mdite --versionFollow Semantic Versioning:
- Patch (0.0.X) - Bug fixes, no API changes, backward compatible
- Minor (0.X.0) - New features, backward compatible, no breaking changes
- Major (X.0.0) - Breaking changes, not backward compatible
Examples:
- Fix a bug →
npm version patch - Add a new command option →
npm version minor - Remove or change command interface →
npm version major
For beta or release candidate versions:
# Create a beta version (e.g., 1.0.0-beta.1)
npm version premajor --preid=beta
# Or manually set version
npm version 1.0.0-beta.1- Check Actions tab for logs
- Verify
NPM_TOKENsecret is valid and not expired - Ensure all tests pass locally:
npm run validate - Check that build succeeds:
npm run build
- Verify package name availability on npm
- Check npm account has publish permissions
- Ensure
.npmignoredoesn't excludedist/directory - Verify provenance is supported (requires npm 9+ and Node 20+)
If you need to recreate a tag:
# Delete local tag
git tag -d v1.0.0
# Delete remote tag (use with caution!)
git push origin :refs/tags/v1.0.0
# Recreate tag
npm version 1.0.0If a release has critical issues:
- Publish a patch version with fix
- Deprecate the broken version on npm:
npm deprecate mdite@1.0.0 "Critical bug, use 1.0.1 instead"
For maintainers setting up automated releases:
# Login to npm
npm login
# Generate an automation token
npm token create --type=automationCopy the token value (you won't be able to see it again).
- Go to repository Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
NPM_TOKEN - Value: Paste the token from step 1
- Click Add secret
Create a test tag to verify the workflow (on a feature branch):
git checkout -b test-release
git tag v0.0.1-test
git push origin v0.0.1-testCheck the Actions tab to see if the workflow runs. Delete the test tag after verification:
git tag -d v0.0.1-test
git push origin :refs/tags/v0.0.1-testIf something is unclear:
- Check existing issues and discussions
- Review the codebase (it's well-documented!)
- Ask in a new discussion or issue
By contributing to mdite, you agree that your contributions will be licensed under the MIT License.
Thank you for contributing to mdite! 🚀