Thank you for your interest in contributing to ScriptHammer. This guide will help you get started.
- Code of Conduct
- Prerequisites
- Development Environment
- Project Structure
- Making Changes
- Component Guidelines
- Testing Requirements
- Submitting Changes
- Style Guide
Be respectful, inclusive, and constructive. We welcome contributors of all experience levels.
- Docker Desktop (required) - All development happens in containers
- Git - Version control
- Code editor - VS Code recommended with Docker extension
Important: Local Node.js/pnpm installation is NOT required for development — every build, test and lint runs inside Docker.
One caveat, because two versions of this guide used to contradict each other on it: the
git hooks in .husky/ execute on the host, not in the container. They shell back into
Docker to do the actual work (.husky/pre-push runs docker compose exec … pnpm run gitleaks), so a host Node install is still not required — but the hooks do need a POSIX
shell and a working docker on your PATH. If you commit from inside the container, as
this repo recommends, that is already true.
# 1. Fork the repository on GitHub
# 2. Clone your fork
git clone https://github.com/YOUR_USERNAME/ScriptHammer.git
cd ScriptHammer
# 3. Add upstream remote
git remote add upstream https://github.com/TortoiseWolfe/ScriptHammer.git
# 4. Create your .env — REQUIRED, the container will not start correctly without it
cp .env.example .env
# 5. Start the development environment
docker compose up -d
# 6. Verify containers are running
docker compose psStep 4 is not optional. docker-compose.yml reads UID and GID from .env so the
container writes files as you rather than as root; without it you get a node_modules and
a .next you cannot delete from the host. .env.example ships UID=1000 / GID=1000,
which is right for most single-user Linux and WSL installs — check with id -u and
id -g and edit only if yours differ.
Copy the file rather than echoing into it. .env.example also carries the Supabase keys,
feature flags and port pins you will want; a redirect (>) silently destroys all of them.
All commands run inside the Docker container:
# Enter the container shell
docker compose exec scripthammer sh
# Or run commands directly
docker compose exec scripthammer pnpm run dev
docker compose exec scripthammer pnpm run test
docker compose exec scripthammer pnpm run lintNever run npm install or pnpm install on your host machine. This violates the Docker-first principle and may cause inconsistencies.
| Command | Description |
|---|---|
docker compose exec scripthammer pnpm dev |
Start the Next.js dev server |
docker compose run --rm builder pnpm build |
Production build — see note |
docker compose exec scripthammer pnpm test |
Run the Vitest suite |
docker compose exec scripthammer pnpm test:coverage |
Generate a coverage report |
docker compose exec scripthammer pnpm lint |
Run ESLint |
docker compose exec scripthammer pnpm format |
Format with Prettier |
docker compose exec scripthammer pnpm format:check |
Check formatting, change nothing |
docker compose exec scripthammer pnpm type-check |
Run TypeScript type checking |
docker compose exec scripthammer pnpm storybook |
Start Storybook |
The build row is run --rm builder, not exec scripthammer, and that is load-bearing
(#293). next dev and next build both own /app/.next. Building inside the dev
container wipes the directory the dev server is serving from, and every route 500s until
it recompiles. The builder service is the same image with its own .next volume, which
is why it exists.
SVG wireframes live under each feature dir (features/<category>/<NNN-name>/wireframes/)
and render via the Next.js /wireframes route. The viewer auto-discovers every
wireframe via a manifest generated by scripts/sync-wireframes.sh on pnpm run dev
and pnpm run build.
docker compose up
# Browse to http://localhost:3000/wireframes (or whatever port compose assigns)Validate wireframes with the shipped 40+ rule validator:
docker compose exec scripthammer \
python3 .specify/extensions/wireframe/scripts/validate.py --all --summaryScriptHammer/
├── features/ # Feature specifications (PRPs)
│ ├── foundation/ # Core features (000-006)
│ ├── core-features/ # Main features (007-012)
│ ├── auth-oauth/ # Auth features (013-016)
│ └── <category>/<NNN-name>/wireframes/ # SVG wireframes, per feature
├── docs/
│ ├── blog/ # Technical blog posts
│ └── interoffice/ # Internal documentation
├── src/ # Application source
│ ├── app/ # Next.js App Router pages
│ ├── components/ # React components (5-file pattern)
│ ├── lib/ # Utility functions
│ └── hooks/ # Custom React hooks
├── .specify/ # SpecKit configuration
│ ├── memory/ # Constitution + inventory
│ └── templates/ # Output templates
└── scripts/ # Automation scripts
Use descriptive branch names with a prefix:
| Prefix | Purpose | Example |
|---|---|---|
feature/ |
New functionality | feature/add-dark-mode |
fix/ |
Bug fixes | fix/login-redirect |
docs/ |
Documentation | docs/update-api-guide |
refactor/ |
Code improvements | refactor/auth-service |
test/ |
Test additions | test/payment-flows |
# Create a new branch
git checkout -b feature/your-feature-nameScriptHammer uses the SpecKit workflow for feature development:
# 1. Specification phase
/speckit.specify # Generate spec.md from feature file
/speckit.clarify # Refine requirements interactively
# 2. Design phase
/wireframe # Generate SVG wireframes
/wireframe-review # Review and classify issues
# Repeat until all wireframes pass
# 3. Implementation phase
/speckit.plan # Generate implementation plan
/speckit.tasks # Generate task breakdown
/speckit.implement # Execute implementation# Fetch upstream changes
git fetch upstream
# Merge into your branch
git checkout main
git merge upstream/main
# Update your feature branch
git checkout feature/your-feature
git rebase mainEvery component MUST have exactly 5 files:
src/components/Button/
├── index.tsx # Re-exports
├── Button.tsx # Component implementation
├── Button.test.tsx # Unit tests (Vitest)
├── Button.stories.tsx # Storybook stories
└── Button.accessibility.test.tsx # Pa11y a11y tests
Use the component generator to ensure compliance:
docker compose exec scripthammer pnpm run generate:component Button// Button.tsx
import { type ButtonHTMLAttributes } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export function Button({
variant = 'primary',
size = 'md',
children,
...props
}: ButtonProps) {
return (
<button className={`btn btn-${variant} btn-${size}`} {...props}>
{children}
</button>
);
}// index.tsx
export { Button } from './Button';
export type { ButtonProps } from './Button';Write tests BEFORE implementation (RED-GREEN-REFACTOR):
- RED: Write a failing test
- GREEN: Write minimal code to pass
- REFACTOR: Improve code while tests pass
| Type | Minimum | Tool |
|---|---|---|
| Unit tests | 60% statements, branches, functions, lines | Vitest |
| E2E tests | Critical paths | Playwright |
| Accessibility | All components | Pa11y |
The single source of truth is vitest.config.ts:154-157; check there before trusting this
table. It said 25% here and 0.5% in a second copy of this guide, against a real threshold
of 60 — neither number was ever true, and both survived because nothing compared the prose
to the config.
# All tests
docker compose exec scripthammer pnpm run test
# Unit tests only
docker compose exec scripthammer pnpm run test:unit
# E2E tests
docker compose exec scripthammer pnpm run test:e2e
# Accessibility tests
docker compose exec scripthammer pnpm run test:a11y
# Watch mode for development
docker compose exec scripthammer pnpm run test:watch// Button.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders children', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('applies variant class', () => {
render(<Button variant="secondary">Secondary</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-secondary');
});
});// Button.accessibility.test.tsx
import { describe, it, expect } from 'vitest';
import { axe, toHaveNoViolations } from 'jest-axe';
import { render } from '@testing-library/react';
import { Button } from './Button';
expect.extend(toHaveNoViolations);
describe('Button Accessibility', () => {
it('has no accessibility violations', async () => {
const { container } = render(<Button>Accessible</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});Knowing this matters, because it tells you what is not checked for you.
pre-commit — two things, in order:
gitleaks protect --staged— secret scan of staged content. A hit blocks the commit.lint-staged—prettier --writetheneslint --fixon*.{js,jsx,ts,tsx}, andprettier --writeon*.{css,md,json}.
pre-push — gitleaks detect over the full history, then the CI gate (including a
production build). This is why a push takes minutes.
It does NOT run your tests or type-check on commit. An older version of this guide
claimed pre-commit ran "related tests" and "TypeScript types"; it never did. Run
pnpm test and pnpm type-check yourself before pushing, or the push gate is where you
will find out.
Never --no-verify. These hooks have caught real secrets in this repo. If one fails,
the output names the file and line — fix it and re-stage.
- Code follows Style Guide
- All tests pass (
docker compose exec scripthammer pnpm run test) - Type check passes (
docker compose exec scripthammer pnpm run type-check) - Linting passes (
docker compose exec scripthammer pnpm run lint) - Build succeeds (
docker compose run --rm builder pnpm build— the builder, #293) - Components follow 5-file pattern
- Storybook stories updated for UI changes (
.stories.tsxis one of the five files) - New features have wireframes reviewed
- Documentation updated if needed
Use clear, descriptive commit messages:
<type>(<scope>): <description>
[optional body]
Co-Authored-By: Claude <name> <noreply@anthropic.com>
Types: feat, fix, docs, style, refactor, test, chore
Examples:
feat(auth): add password reset flow
Implements forgot password and reset password pages with
email verification via Supabase Auth.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>fix(messaging): resolve group chat notification bug
Messages in group chats now correctly trigger notifications
for all members except the sender.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>-
Push your branch
git push origin feature/your-feature
-
Create PR on GitHub
- Use a descriptive title
- Fill out the PR template
- Link related issues
-
PR Description Template
## Summary Brief description of changes (1-3 bullet points) ## Changes - Added X - Modified Y - Removed Z ## Testing - [ ] Unit tests added/updated - [ ] E2E tests added/updated - [ ] Manual testing completed ## Screenshots (if applicable) ## Related Issues Closes #123
-
Address Review Feedback
- Respond to all comments
- Push fixes as new commits
- Request re-review when ready
-
Merge Requirements
- The two required status checks pass:
Test (20.x)andaccessibility mainis protected, so a PR is the only route in — direct pushes are rejected for everyone, admins included
Branch protection does not require an approving review, and does not require conversations to be resolved. Request a review when the change warrants one, but nothing blocks the merge while you wait for it.
Only those two checks are required because they are the two workflows with no
paths:filter, so they report on every PR. A required check that never reports is pending forever rather than skipped, which would make any docs-only PR permanently unmergeable. The other workflows still run and should still be green. - The two required status checks pass:
- Use strict mode (
"strict": true) - Prefer interfaces over types for objects
- Export types alongside components
- Use explicit return types on functions
// Good
export interface UserProps {
name: string;
email: string;
}
export function User({ name, email }: UserProps): JSX.Element {
return (
<div>
{name} ({email})
</div>
);
}
// Avoid
export type UserProps = { name: string; email: string };
export const User = ({ name, email }) => <div>{name}</div>;- Use functional components with hooks
- Prefer named exports
- Keep components focused (single responsibility)
- Use composition over inheritance
- Use Tailwind utilities for styling
- Use DaisyUI components when available
- Follow mobile-first responsive design
- Ensure 44px minimum touch targets
// Good - mobile-first
<button className="p-4 text-sm md:text-base lg:p-6">
Click
</button>
// Avoid - desktop-first
<button className="p-6 lg:p-4">
Click
</button>- All interactive elements must be keyboard accessible
- Use semantic HTML (
<button>,<nav>,<main>) - Include ARIA labels where needed
- Maintain color contrast ratios (WCAG AA)
- Test with screen readers
This is a template. scripts/rebrand.sh rewrites the project name across 200+ files
when someone forks it, so any literal you add that must keep naming the upstream
project needs to say so. Mark it with rebrand:keep in a comment on the same line:
href: 'https://github.com/TortoiseWolfe/ScriptHammer', // rebrand:keep- Line-scoped, not file-scoped. A marker at the top of a file protects nothing below it.
- The token is deliberately brand-neutral.
scripthammer:keepwould contain the very string the rebrand searches for. - Prettier keeps a trailing
// rebrand:keepon its own line, so formatting will not separate the marker from what it protects.
Typical cases are upstream URLs, the attribution link, and anything naming this repository as the source rather than as the current project. When in doubt, ask whether a fork would still want that line to name the upstream project — if yes, mark it.
Note that this section is written to survive its own advice: it describes the rule without embedding the project name in a sentence that a rebrand would falsify. That is usually easier than marking prose line by line.
See docs/FORKING.md for the fork-side view.
- Questions: Open a Discussion
- Bugs: Open an Issue
- Security: See SECURITY.md for the reporting process — do not open a public issue
- Search existing issues, including closed ones — a surprising number of things here were fixed and closed rather than never reported.
- Reproduce in a clean environment (
docker compose down && docker compose up).
Include: a descriptive title, steps to reproduce, expected vs actual behaviour, any error output, and your environment (OS, browser, Docker version).
Two issue templates exist — .github/ISSUE_TEMPLATE/accessibility.md and
optimization.md. Anything else is a blank issue; that is fine.
By contributing, you agree that your contributions will be licensed under the same license as the project.
Thank you for contributing to ScriptHammer!