Migrate from ESLint

opensip-cli doesn't replace ESLint. The two tools coexist comfortably in the same CI pipeline — ESLint handles language-specific syntactic patterns inside one file; opensip-cli handles architectural, cross-file, and cross-language rules that ESLint can't express. This guide walks the practical question: given an ESLint rule we currently enforce, where does it belong?

The decision tree

┌─ Is the rule about a single language's syntax / idioms?
│  (no-unused-vars, prefer-const, formatting, JSX idioms)
│     → Keep it in ESLint. opensip-cli won't help here.
│
├─ Is the rule about a banned API or pattern within one file?
│  (no-console, no-eval, no-debugger, no-restricted-imports)
│     → Either works. ESLint is fine. opensip-cli wins if you want
│       to enforce the same ban across TypeScript + Python + Go.
│
├─ Is the rule about file structure or cross-file relationships?
│  (every package has a README, no circular imports, no cross-layer
│  imports between architectural layers)
│     → opensip-cli. ESLint can't see across files coherently.
│
├─ Is the rule about cross-language consistency?
│  (no console.log in any language, every public function has docs,
│  cross-language API contracts match)
│     → opensip-cli. ESLint is language-locked.
│
└─ Is the rule about static call-graph shape?
   (orphan code, dead paths, duplicated function bodies)
      → opensip-cli `graph` (different command). ESLint can't.

What to move, what to keep

| Type of rule | ESLint | opensip-cli | Notes |

|---|---|---|---|

| no-unused-vars, prefer-const, formatting | ✓ keep | — | Linter sweet spot. |

| no-console, no-debugger, no-eval | ✓ keep | could move | Either works. Move if you want one rule across languages. |

| no-restricted-imports (in-file) | ✓ keep | could move | ESLint's is faster; opensip-cli's is more flexible. |

| "No module under packages/cli/ may import from packages/checks-*" | ✗ can't | ✓ — sweet spot | This is the canonical architectural rule. |

| "Every package directory has a README" | ✗ can't | ✓ | File-structure rule. ESLint doesn't see directories. |

| "No circular imports across packages" | partial | ✓ better | ESLint plugins exist but break on monorepo boundaries. |

| "No FIXME comments in packages/api/" | ✓ (eslint-plugin-no-todo) | ✓ | Either works. opensip-cli wins if you want different rules per directory. |

| "Cross-language: no console.log / print / fmt.Println" | ✗ language-locked | ✓ — sweet spot | One rule, polyglot. |

| Static call-graph: orphan code, dead paths | ✗ | ✓ (graph) | ESLint can't see across files. |

The honest summary: ESLint stays in your toolchain. opensip-cli adds capabilities ESLint doesn't have, but doesn't replace ESLint's core competence.

A coexisting CI pipeline

# .github/workflows/ci.yml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: pnpm i --frozen-lockfile
      - run: pnpm lint            # ESLint — syntactic, in-file rules

  fit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 24 }
      - run: curl -fsSL https://opensip.ai/cli/install.sh | bash
      - run: opensip fit --gate-compare
      - run: opensip fit export --format baseline --out fit.sarif
        if: always()
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: fit.sarif, category: opensip-fit }

Two jobs, run in parallel. ESLint's output lands in the PR via its own conventions (or actions/eslint-annotate); opensip-cli's lands via the SARIF upload.

Don't double-enforce

When you do move a rule from ESLint to opensip-cli, disable it in ESLint. Running the same rule in both tools doubles findings on every PR, doubles the maintenance burden, and confuses developers about which tool owns it.

Worked example: you decide no-console should be polyglot across your TS + Python + Go codebase, so you write an opensip-cli check for it. In ESLint, set the rule to off:

// .eslintrc.json
{
  "rules": {
    "no-console": "off",   // moved to opensip-cli (polyglot)
    "no-unused-vars": "error",
    // ...
  }
}

Document the move in a code comment or a MIGRATIONS.md so the next engineer doesn't reintroduce the ESLint rule.

Migrating gradually

You don't need to move everything at once. The pragmatic path:

Some teams never move anything from ESLint — they just add opensip-cli as a layer above and keep ESLint untouched. That's a totally valid end state.

Where to go next

| You want to … | Go to … |

|---|---|

| See concrete code for a banned-API check (which ESLint also handles) | Ban an API pattern |

| See the full ESLint / Semgrep / Sonarqube / Snyk comparison | vs. other tools |

| Add opensip-cli to your CI pipeline | Wire into CI |

| Write your first architectural rule | Write your first check |