Write your first check
By the end of this page you'll have: an installed CLI, an opensip-cli.config.yml, one custom check you wrote, a recipe that runs it, and a passing-or-failing exit code that CI can gate on. ~15 minutes if you're new to the tool; less if you've seen the show-me page.
1. Scaffold a project
curl -fsSL https://opensip.ai/cli/install.sh | bash
cd your-project
opensip init
init detects your language(s) and scaffolds:
your-project/
├── opensip-cli.config.yml
└── opensip-cli/
├── fit/
│ ├── checks/example-check.mjs
│ └── recipes/example-recipe.mjs
└── sim/
└── …
Confirm it works:
opensip fit --recipe example
# 1 Passed, 0 Failed | Duration 0.1s
If that exits 0, the platform is wired correctly. Delete or edit the example as you like — the rest of this page replaces it.
2. Write your check
Pick something your team cares about. We'll do "no FIXME comments left in production code" — small enough to fit on this page, real enough to demonstrate the moving parts.
Create opensip-cli/fit/checks/no-fixme.mjs:
import { defineCheck } from '@opensip-cli/fitness';
export default defineCheck({
// UUID v4 — stable across renames. Generate with:
// node -e "console.log(crypto.randomUUID())"
id: '0a0a0a0a-0a0a-4a0a-8a0a-0a0a0a0a0a0a',
slug: 'no-fixme-comments',
description: 'No FIXME comments left in source',
tags: ['quality', 'documentation'],
scope: { languages: ['typescript'], concerns: [] },
contentFilter: 'raw', // see comments — don't strip them
analyze(content, filePath) {
const violations = [];
content.split('\n').forEach((line, idx) => {
if (/\bFIXME\b/.test(line)) {
violations.push({
line: idx + 1,
message: `FIXME comment found: ${line.trim()}`,
severity: 'warning',
});
}
});
return violations;
},
});
Five fields you'll touch in every check:
id— UUID v4, never changes. Baselines key on this; renaming the slug doesn't break gates.slug— kebab-case, human-readable. Users pass it to--check <slug>.description— one-line summary. Shown in--listand the dashboard.tags— recipes select checks by tag ({ type: 'tags', include: ['quality'] }).scope— what kind of code this is for. Filters which files the check runs against.
analyze(content, filePath) returns an array of violations. Empty = the check passed for that file.
3. Confirm it loads
opensip fit --list
Your check appears in the list. If it doesn't, check:
- File is under
opensip-cli/fit/checks/(nested category directories are fine; the.mjsextension matters — it's how the loader identifies plugins) export default defineCheck(...)(default export — not a named one)- No syntax errors (
node opensip-cli/fit/checks/no-fixme.mjswill surface them)
4. Run it
opensip fit --check no-fixme-comments
Output:
✗ no-fixme-comments 312 files, 2 violations
0 Passed, 1 Failed (2 Errors, 0 Warnings) | Duration 0.4s
> echo $?
1
Add --verbose to see each violation's file + line:
opensip fit --check no-fixme-comments --verbose
If you wanted to clean up the violations first and gate on new ones only, this is where the baseline flow kicks in — see wire into CI and adopt in a monorepo.
5. Add it to a recipe
A recipe is a named lineup of checks plus execution options. Create opensip-cli/fit/recipes/quality.mjs:
export const recipes = [{
id: 'URCP_quality',
name: 'quality',
displayName: 'Quality',
description: 'Code-quality checks',
checks: { type: 'tags', include: ['quality'] },
execution: { mode: 'parallel', stopOnFirstFailure: false, timeout: 30_000 },
reporting: { format: 'table', verbose: false },
}];
checks: { type: 'tags', include: ['quality'] } picks every check tagged quality — including your new no-fixme-comments check and any built-in checks with that tag. Other selectors:
{ type: 'all' }— every enabled check{ type: 'pattern', include: ['no-*'] }— slug glob{ type: 'explicit', checkIds: ['no-fixme-comments', …] }— exact slug list
Run the recipe:
opensip fit --recipe quality
6. Lock in a CI gate
Run once to capture the current state as the baseline:
opensip fit --recipe quality --gate-save
From now on, in CI:
opensip fit --recipe quality --gate-compare
--gate-compare exits 0 if no new violations appeared (existing ones are tolerated), non-zero otherwise. That's the incremental-adoption flow — you don't have to fix the baseline before turning the gate on.
The full GitHub Actions example is in wire into CI.
Where to go next
| You want to … | Go to … |
|---|---|
| Ban a specific API in your codebase | Ban an API pattern |
| Add the GitHub Actions step | Wire into CI |
| Graduate from .mjs files to a TypeScript workspace pack | Adopt in a monorepo |
| Reference the full check API | Plugin authoring |
| See every built-in check | Checks reference |