Skip to content

FlagLint Blog

LaunchDarkly Monorepo Migration to OpenFeature: Migrate One Package at a Time

Your monorepo has a dozen services, a shared feature-flag wrapper, and three years of direct LaunchDarkly SDK calls spread across packages. You’ve decided to move to OpenFeature, but nobody wants to touch fifteen services at once. One broken migration branch stalls the entire team.

The right approach for a LaunchDarkly monorepo migration is incremental: audit one service, preview the rewrites, apply, enforce in CI, move to the next. FlagLint is designed for exactly this pattern — it operates on a single directory per invocation and reads a per-package .flaglintrc, so audit and migrate commands stay scoped to one service’s conventions without touching anything else.

This guide walks through the full cycle for one service in a Turborepo or Nx workspace.

A single-package repo has one import path for the LaunchDarkly SDK, one shared client, and one set of call sites. A monorepo doesn’t.

Import paths diverge per package. services/checkout might import @company/feature-flags. services/pricing imports a different internal wrapper. Both ultimately call the LaunchDarkly SDK, but through different module paths.

Shared clients live in internal packages. A packages/feature-flag-client workspace package often exports an OpenFeature-shaped wrapper around the LaunchDarkly SDK. FlagLint needs to know this binding to classify call sites correctly — otherwise it reports false flag debt on wrapper calls that are already vendor-neutral.

Migration risk varies by service. One service evaluates only simple boolean flag keys — every call site is automatable. Another has dynamic key construction and bulk allFlagsState calls. Your migration plan should reflect that spread before you write a single line.

Running any migration tool at the workspace root treats all of that as uniform. A LaunchDarkly monorepo migration needs service-level granularity.

A typical Turborepo layout:

services/
checkout/
src/
routes/checkout.ts
platform/feature-flags.ts
.flaglintrc
package.json
pricing/
src/
.flaglintrc
packages/
feature-flag-client/
src/
index.ts # shared OpenFeature wrapper

Each service carries its own .flaglintrc. FlagLint reads that file when you pass --config, keeping every command scoped to one package’s import conventions.

Start with the service you know best. Run flaglint audit against its source directory:

Terminal window
npx flaglint audit ./services/checkout/src \
--config ./services/checkout/.flaglintrc

The output is a flag debt inventory: every call site classified by call type (boolean variation, string variation, JSON variation, detail evaluation), with a readiness score for each that tells you whether FlagLint can rewrite it automatically or whether it needs manual review.

Here is what FlagLint reports when a source directory contains no LaunchDarkly SDK usage:

- Auditing ./src/...
No LaunchDarkly SDK usage detected in 1 files.

That is your target state for every service. Once a package reaches that output, it carries zero flag debt and the LaunchDarkly SDK dependency can be removed from that package entirely.

Create a .flaglintrc in each service root. The most important section for monorepos is openFeatureClientBindings, which tells FlagLint how the OpenFeature client is imported in that package:

services/checkout/.flaglintrc
{
"include": ["**/*.{ts,js}"],
"exclude": [
"**/node_modules/**",
"**/dist/**",
"**/*.test.ts",
"**/*.spec.ts"
],
"openFeatureClientBindings": [
{
"importName": "openFeatureClient",
"modulePatterns": ["**/platform/feature-flags"]
}
]
}

If your shared client lives in a workspace package, add its import pattern:

{
"openFeatureClientBindings": [
{
"importName": "featureFlagClient",
"modulePatterns": ["**/feature-flag-client/src/index"]
}
]
}

Without this, FlagLint cannot distinguish a direct LaunchDarkly SDK call from a call through your internal wrapper. The audit reports false flag debt on wrapper call sites, inflating your readiness score estimate and causing the rewriter to touch files it shouldn’t.

For the full per-package configuration reference, see the monorepo guide.

Before writing any file, run flaglint migrate in dry-run mode. This produces the exact before/after diff for every call site FlagLint can safely rewrite, without touching disk:

Terminal window
npx flaglint migrate ./services/checkout/src \
--config ./services/checkout/.flaglintrc \
--dry-run

The diff shows three things per call site: the flag key being migrated, the call type being replaced, and the OpenFeature provider equivalent it becomes.

Any call site that carries a staleness signal — a flag key constructed at runtime, a variationDetail call, or a bulk allFlagsState call — is marked for manual review rather than automated rewrite. The stale signal classification is carried through from the audit; the dry-run makes it visible in diff form before you commit to anything.

Check the diff carefully. If it touches more files than the audit suggested, review your openFeatureClientBindings pattern first — a too-broad module pattern causes over-reporting. The readiness score in the audit tells you what percentage of call sites are automatable; the dry-run confirms the exact set.

Open a branch scoped to this one service, then apply:

Terminal window
git checkout -b migrate/checkout-openfeature
npx flaglint migrate ./services/checkout/src \
--config ./services/checkout/.flaglintrc \
--apply

FlagLint rewrites only the call sites it classified as automatable. Each rewrite replaces a direct LaunchDarkly SDK call with the OpenFeature equivalent, preserving flag key, fallback value, and return type. The argument-order difference — LaunchDarkly puts the default value last; OpenFeature puts it second — is handled by the rewriter. This is the class of bug that grep-based migration scripts consistently miss. See why argument order breaks production migrations for the detailed breakdown.

Verify the service starts and its tests pass before opening the PR. Keep the migration branch scoped to one service — nothing else changes.

Once a service’s migration branch merges, lock it. The flaglint validate command fails the build if any new direct LaunchDarkly SDK call appears in that source directory:

Terminal window
npx flaglint validate ./services/checkout/src \
--config ./services/checkout/.flaglintrc \
--no-direct-launchdarkly

In GitHub Actions, run this as a matrix across migrated services:

name: OpenFeature boundary check
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
strategy:
matrix:
package: [checkout, pricing, inventory]
steps:
- uses: actions/checkout@v4
- name: Enforce OpenFeature boundary — ${{ matrix.package }}
run: |
npx flaglint validate ./services/${{ matrix.package }}/src \
--config ./services/${{ matrix.package }}/.flaglintrc \
--no-direct-launchdarkly

Add a service to the matrix as soon as its migration branch merges. From that point forward, CI blocks any regression to the LaunchDarkly SDK on that service. For a complete CI walkthrough, see enforcing your migration in GitHub Actions.

Four call types require hand-editing and are excluded from automated rewrites:

Dynamic flag keysldClient.variation(buildKey(userId, 'checkout-v2'), ...) cannot be safely rewritten. The flag key is not a string literal at the call site. FlagLint marks it with a stale signal and surfaces it for manual review.

allFlagsState calls — No direct OpenFeature provider equivalent. These are typically used for bootstrapping client-side applications and require an architectural decision before rewriting.

variationDetail calls — Returns a reason object with no standard equivalent across OpenFeature providers. Plan this call type explicitly in your migration plan before touching it.

Configured wrappers with non-standard argument order — If your internal wrapper swaps the flag key and default value positions compared to the LaunchDarkly SDK, the rewriter detects a staleness signal and surfaces the call site for review rather than rewriting it automatically.

The manual review patterns guide covers each of these with before/after examples.

A LaunchDarkly monorepo migration is a sequence, not a parallel operation. A practical order:

  1. Run flaglint audit across all services. Rank by readiness score — highest first.
  2. Migrate the highest-readiness service. Enforce in CI before moving to the next.
  3. For each subsequent service, run --dry-run to estimate effort before scheduling the work.
  4. For services with manual call types, open a tracking issue listing the flag keys that need hand-editing.
  5. Once every service passes flaglint validate --no-direct-launchdarkly, remove the LaunchDarkly SDK from the workspace root package.json.

Teams that have tried to migrate all services in one branch report the same three failure modes: merge conflicts across services, a broken shared client that blocks everything, and a diff so large that reviewers stop reviewing. The per-service approach eliminates all three. Each merged branch is a verified, independently deployable step — the migration history is visible in your commit graph.

For a per-package configuration reference including workspace-level CI patterns, see the monorepo guide.

flaglint-go: We Field-Tested It Against Real Repos and Found Zero Recall

flaglint-go is a native Go binary for auditing LaunchDarkly Go server SDK usage — no Node.js required.

Terminal window
brew install flaglint/tap/flaglint-go
flaglint-go audit ./services

It’s the Go-native counterpart to flaglint-js, and it shares the same non-negotiable rule: a variable is only ever treated as a LaunchDarkly client when its identity can be proven — never by matching a method name in isolation. That rule is why flaglint-js earned trust in the first place, and flaglint-go inherited it from day one.

Before shipping, we validated flaglint-go the way you’d hope any static analysis tool gets validated: not just against synthetic fixtures we wrote ourselves, but against real, unmodified, open-source Go repositories known to use the LaunchDarkly SDK — including the official launchdarkly-labs/ld-sample-app-go, plus weaviate/weaviate, CMS-Enterprise/mint-app, and e2b-dev/infra.

The result: zero false positives, but also zero recall on every single repo with genuine usage.

That’s a striking failure. Not “missed a few edge cases” — missed all of it, on the official sample app included. The scanner wasn’t broken; it was doing exactly what it was designed to do (prove identity syntactically, never guess by name) — it just turned out real Go code almost never wires up the LaunchDarkly client the simple way our synthetic fixtures assumed.

Three different repos, three different indirection patterns, none of them exotic:

The official sample app wires the client through a package-level singleton getter, called from a different package entirely:

// package ldclient
func GetLdClient() *ld.LDClient { /* ... */ }
// package api, a different file, a different package
client := ldclient.GetLdClient()
client.BoolVariation("test-flag", ctx, false)

weaviate stores the client into a wrapper struct via composite literal, then reaches it through a two-level field chain — on a generic struct:

type LDIntegration struct {
ldClient *ld.LDClient
}
type FeatureFlag[T SupportedTypes] struct {
ldInteg *LDIntegration
}
// inside one of FeatureFlag[T]'s methods:
flag, err := f.ldInteg.ldClient.StringVariation(f.key, f.ldInteg.ldContext, v)

mint-app passes an already-constructed client into a struct’s constructor as a plain function parameter — there’s no assignment to trace at all; the parameter’s declared type is the only place identity is ever established.

None of these require real type-checking to resolve. In every case, the proof of identity is available directly from the AST — a struct’s declared field type, a function’s declared parameter or return type — the same “trust the syntax, no build required” spirit as the rest of the scanner. What they do require is looking at more than one file at a time, since the code that constructs the client and the code that uses it are routinely in a different file, sometimes a different package, from wherever the binding was first established.

We rearchitected the scanner from a per-file model (read, parse, detect, discard — one file at a time) into a whole-scan pass: parse every file up front, then resolve identity across the entire scan before any detection runs. That closed all three gaps:

  • Composite-literal struct-field binding&LDIntegration{ldClient: client} binds the field when client is itself already bound.
  • Multi-level field-selector chains, including through generics — a struct’s fields can be declared in a different file than where they’re used.
  • Cross-package factory/getter functions, resolved via real go.mod-derived import paths — never a name-based guess. (We explicitly considered and rejected matching by “last segment of the import path” as a shortcut — that’s a name heuristic wearing an import-path costume, exactly the kind of thing our non-negotiable identity rule exists to prevent.)
  • Parameter-typed client bindings — a parameter declared *ld.LDClient is bound from its type alone, no assignment to trace.

We also found and fixed a bug that had nothing to do with the original plan: Go generics. weaviate’s FeatureFlag[T] broke a piece of code that had only ever been tested against non-generic structs — a method receiver on a generic type has a different AST shape (*ast.IndexExpr) than a plain one, and the scanner silently failed to resolve it at all. Found only by testing against real code that happened to use generics.

Before merging any of this, we ran an independent review pass — a fresh reviewer with no context on the implementation, adversarially checking the diff. It found something real: two of the new whole-scan indices (struct field types, and package-level/struct-field bindings) were keyed by bare name across the entire scan, not scoped to a package. Go allows two completely unrelated packages to each declare their own Service struct with their own Client field — and a genuinely-bound client in one package would have incorrectly matched a same-named, unconnected field in a totally different package.

That’s exactly the class of false positive our whole identity model exists to prevent, and it slipped through the first pass. We fixed it by partitioning every whole-scan index by package — matching how an unqualified identifier is only ever visible within its own package in real Go anyway — added regression fixtures reproducing the exact collision, and re-verified against all three real repos to confirm detection was unaffected by the fix.

After the fix, we re-cloned and re-scanned the same repos:

$ flaglint-go audit ./ld-sample-app-go
Scan complete — 1 unique flag(s) across 1 call site(s) (3 file(s))
Migration readiness: 100/100 · ready
1 low risk · 0 medium risk · 0 high risk
$ flaglint-go audit ./weaviate
Scan complete — 0 unique flag(s) across 4 call site(s) (4512 file(s))
Migration readiness: 0/100 · complex
0 low risk · 0 medium risk · 4 high risk

weaviate’s four call sites all show as high-risk dynamic keys — correctly so, since the flag key there (f.key) is a runtime struct field, not a string literal. That’s the honest answer, not a false claim of full static resolution.

We’re not going to pretend this closes every gap. e2b-dev/infra’s usage is still undetected — it takes a bound client’s method value and passes it through a generic helper function, which is a genuinely harder problem (interprocedural data-flow, not just “look at more files”). That, along with a handful of narrower gaps found along the way, is filed as tracked, public issues rather than silently swept under the rug — see the Supported Scope and Limitations pages for the full, honest list.

Terminal window
brew install flaglint/tap/flaglint-go
flaglint-go audit ./services

LaunchDarkly Feature Flag Cleanup: Audit, Rewrite, and Enforce in TypeScript

Your codebase has been accumulating direct LaunchDarkly SDK calls for years. The team knows a cleanup is overdue, but nobody has a clear picture of how many flag keys exist, which ones are safely automatable, and which ones will bite you if touched carelessly. LaunchDarkly’s built-in cleanup tooling — Vega — requires an Enterprise subscription. Grep finds string literals but misses dynamic keys, detail evaluations, and bulk calls. You end up either doing nothing or hand-editing files one at a time and hoping the argument order is correct.

FlagLint is a free, open-source CLI that automates LaunchDarkly feature flag cleanup in TypeScript and JavaScript codebases using AST-based static analysis — not regex — to classify every direct LaunchDarkly SDK call by risk, generate a readable flag debt inventory, rewrite safe call sites to the OpenFeature standard, and enforce the resulting boundary in CI. No LaunchDarkly API key required.

This guide walks through a complete cleanup cycle: audit → rewrite → enforce.

The first step of any LaunchDarkly feature flag cleanup is understanding what you are dealing with. Run flaglint audit against your source directory:

Terminal window
npx flaglint audit ./src

Here is the real output for a two-file Node.js service with seven flag evaluations across checkout and pricing modules:

- Auditing ./src...
# FlagLint Audit Report
**Scanned at:** 2026-07-06T04:47:44.977Z
**Scan root:** ./src
**Files scanned:** 2
**Duration:** 35ms
## Summary
| Total Flags | High Risk | Medium Risk | Total Usages |
|-------------|-----------|-------------|--------------|
| 7 | 2 | 5 | 7 |
| Dynamic Keys | Detail Evals | Bulk Calls | Stale Signals | Safely Automatable | Manual Review |
|--------------|--------------|------------|---------------|-------------------|---------------|
| 1 | 1 | 0 | 0 | 5 | 2 |
> **Staleness:** No staleness signals detected. Heuristics checked: keyword match
> (flag key contains old/deprecated/legacy/temp/tmp/test/demo), path pattern
> (test/spec/mock files, deprecated/old/legacy directories), and minFileCount
> threshold. Git-history-based staleness (last evaluation date) requires git
> metadata and is not available in a pure static scan.
## Migration Readiness
Migration readiness: **71/100** · moderate
[██████████████████░░░░░░░] 71%
5 safely automatable · 2 require manual review
## Flag Debt Inventory
| Flag Key | Risk | Usages | Files | Call Types | Reasons |
|----------|------|--------|-------|------------|---------|
| `<dynamic key>` | 🔴 High | 1 | 1 | numberVariation | dynamic key |
| `beta-pricing` | 🔴 High | 1 | 1 | boolVariationDetail | detail evaluation |
| `checkout-v2` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
| `discount-percentage` | 🟢 Automatable | 1 | 1 | numberVariation | safely automatable |
| `ui-theme` | 🟢 Automatable | 1 | 1 | stringVariation | safely automatable |
| `checkout-config` | 🟡 Medium | 1 | 1 | jsonVariation | safely automatable, json variation |
| `promo-banner` | 🟢 Automatable | 1 | 1 | boolVariation | safely automatable |
## Next Steps
- Run `flaglint migrate --dry-run` to preview safe OpenFeature rewrites
- Run `flaglint validate --no-direct-launchdarkly` to enforce OF boundary in CI
- Review HIGH risk flags manually before any automated migration
✓ Audit complete: 7 flags — 2 high risk, 5 medium risk (35ms, 2 files)
Migration readiness: 71/100 · moderate
[██████████████████░░░░░░░] 71%
5 safely automatable · 2 require manual review

The readiness score of 71/100 means 5 of 7 call sites can be rewritten automatically. The two high-risk entries need manual attention before anything else moves.

Add --format html --output flag-debt.html to produce a shareable report to attach to a migration planning ticket. The flag debt blog post covers the full range of audit options including effort estimates.

What the flag debt inventory is telling you

Section titled “What the flag debt inventory is telling you”

Two call types are classified as high risk:

Dynamic key (<dynamic key>) — the flag key is constructed at runtime from a variable or template literal (e.g., const key = `pricing-${plan}`). FlagLint cannot resolve which flag is actually evaluated at any given call site. Any automated rewrite here would silently touch the wrong call. These require a human decision: extract the dynamic key into a lookup table, split into separate static flag keys, or handle manually per call type.

Detail evaluation (beta-pricing via boolVariationDetail) — variationDetail returns a reason object with no direct OpenFeature equivalent. FlagLint skips these by design. You need to decide whether that reason metadata is still needed after migration, and if so, which OpenFeature detail API maps to your use case.

The five remaining call sites — boolVariation, numberVariation, stringVariation, jsonVariation, and a second boolVariation — are all safely automatable. FlagLint can rewrite every one of them without you touching a line.

Run flaglint migrate with --dry-run to see exactly what changes before any file is modified:

Terminal window
npx flaglint migrate ./src --dry-run

Real output (provider setup guidance section omitted; covered in Step 3 below):

- Scanning ./src...
LaunchDarkly usages found: 7
Safely automatable: 5 · Manual review: 2
Reviewable diffs: 5
Diffs requiring provider setup: 5
Skipped usages: 2
## Diffs
diff --git a/checkout.ts b/checkout.ts
--- a/checkout.ts
+++ b/checkout.ts
@@ -8,1 +8,1 @@
- const newCheckoutEnabled = await ldClient.boolVariation("checkout-v2", ctx, false);
+ const newCheckoutEnabled = await openFeatureClient.getBooleanValue("checkout-v2", false, ctx);
@@ -9,1 +9,1 @@
- const discountPct = await ldClient.numberVariation("discount-percentage", ctx, 0);
+ const discountPct = await openFeatureClient.getNumberValue("discount-percentage", 0, ctx);
@@ -10,1 +10,1 @@
- const theme = await ldClient.stringVariation("ui-theme", ctx, "default");
+ const theme = await openFeatureClient.getStringValue("ui-theme", "default", ctx);
@@ -11,1 +11,1 @@
- const config = await ldClient.jsonVariation("checkout-config", ctx, {});
+ const config = await openFeatureClient.getObjectValue("checkout-config", {}, ctx);
diff --git a/pricing.ts b/pricing.ts
--- a/pricing.ts
+++ b/pricing.ts
@@ -10,1 +10,1 @@
- const promoEnabled = await ldClient.boolVariation("promo-banner", ctx, false);
+ const promoEnabled = await openFeatureClient.getBooleanValue("promo-banner", false, ctx);
## Skipped Usages
- pricing.ts:9:26 — `dynamicKey` via `numberVariation`: dynamic key requires manual review
- pricing.ts:11:23 — `beta-pricing` via `boolVariationDetail`: detail methods skipped:
OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature detail result parity requires
manual review

Notice the argument order flip: boolVariation("checkout-v2", ctx, false) becomes getBooleanValue("checkout-v2", false, ctx). The LaunchDarkly SDK puts context second and default last; OpenFeature reverses that. This reversed argument order is the most common source of silent production bugs in manual migrations — FlagLint handles it correctly for every safe call type.

The jsonVariationgetObjectValue rewrite is flagged json variation in the audit because OpenFeature’s JSON type is object. If your LaunchDarkly flag ever returns a primitive JSON value (number, string, boolean, null), call semantics differ. Review before applying.

The two skipped usages are left exactly as-is in source.

The dry-run output marks all five diffs as requiring provider setup. The LaunchDarkly SDK stays as your evaluation backend — you are changing the API your application code calls, not where flags are stored or evaluated.

Install once:

Terminal window
npm install @openfeature/server-sdk \
@launchdarkly/node-server-sdk \
@launchdarkly/openfeature-node-server

Add a bootstrap file at application startup. Do not remove existing LaunchDarkly packages — the OpenFeature provider depends on them at runtime:

import { OpenFeature } from "@openfeature/server-sdk";
import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);
await OpenFeature.setProviderAndWait(ldProvider);
export const openFeatureClient = OpenFeature.getClient();

Import openFeatureClient in every module that has call sites in the migration plan, or configure openFeatureClientBindings in your .flaglintrc so FlagLint locates the client binding automatically. The add OpenFeature provider tutorial covers both approaches with full examples.

Once the OpenFeature provider is wired:

Terminal window
npx flaglint migrate ./src --apply

FlagLint rewrites only the five safely automatable call sites and leaves the two high-risk ones untouched. What you get is an ordinary git diff: five function-call replacements across two files, reviewable like any other PR. The dynamic key and detail evaluation remain as direct LaunchDarkly SDK calls until you handle them manually.

After --apply and manual resolution of the remaining two call sites, lock the boundary so no new direct LaunchDarkly SDK calls can reach main:

Terminal window
npx flaglint validate ./src --no-direct-launchdarkly

If you are mid-cleanup and cannot enforce a hard block yet, use baseline mode: it freezes existing flag debt and fails on any net-new addition.

Terminal window
# Write current findings as the accepted baseline
npx flaglint audit ./src --write-baseline .flaglint-baseline.json
# In CI: fail only on findings not present in the baseline
npx flaglint validate ./src \
--no-direct-launchdarkly \
--baseline .flaglint-baseline.json \
--fail-on-new

Commit .flaglint-baseline.json to source control. Each time you resolve a flag through migrate --apply or a manual cleanup, re-run --write-baseline to shrink the accepted set. The GitHub Actions integration guide shows the full CI step configuration, including SARIF upload for GitHub Code Scanning annotations.

If your LaunchDarkly SDK calls are spread across multiple packages, run the three commands per package rather than at the repo root. Each package can have its own .flaglintrc pointing to its local OpenFeature client binding. The monorepo guide covers per-package configuration and how to sequence the cleanup when the same flag key is evaluated in shared libraries and consumer apps simultaneously.

LaunchDarkly feature flag cleanup at the code level breaks into four repeatable steps:

  1. flaglint audit ./src — inventory your flag debt and get a readiness score
  2. flaglint migrate ./src --dry-run — review the migration plan before touching files
  3. flaglint migrate ./src --apply — apply safe rewrites; fix the remaining two manually
  4. flaglint validate ./src --no-direct-launchdarkly — gate the boundary in CI

No Enterprise subscription, no API key, no manual grep. The complete six-step migration walkthrough picks up from here if you want to see the full picture across a production Node.js service.

FlagLint Is Now Installable via Homebrew

Starting with v1.1.0, FlagLint is installable via Homebrew — no Node.js required.

Terminal window
brew tap flaglint/tap
brew install flaglint

That’s it. The full CLI is available immediately, including audit, scan, migrate, and validate.

Until now, the only install paths were npm install -g flaglint or npx flaglint@latest. Both work fine — but both require Node.js 20 or newer. That’s a reasonable assumption for application developers, but it’s a friction point in a few common situations.

DevOps and platform engineers often run tooling audits across repos without a Node.js environment set up. Installing Node just to run one CLI is the kind of thing that gets a tool skipped in favour of whatever’s already available.

Docker-based CI is the bigger one. A lot of teams run their CI in minimal images — no Node, no npm. Adding a Node install step purely to run npx flaglint adds 30-60 seconds to every pipeline run and pulls in a dependency that has nothing to do with the actual application. With Homebrew, a Linux CI job can install FlagLint in a single brew install call with no Node dependency.

Mac developers who use Homebrew for CLI tools now get brew upgrade flaglint like any other tool, without thinking about npm.

The tap lives at github.com/flaglint/homebrew-tap. The formula fetches the published npm tarball directly from the npm registry and wires up the CLI binary.

The formula updates automatically on every release — a GitHub Actions job in the main repo runs after each npm publish, computes the new tarball SHA256, and commits an updated formula to the tap. So brew upgrade flaglint will always pull the current release without any manual intervention.

Terminal window
brew tap flaglint/tap
brew install flaglint
flaglint --version

Or if you already have Node.js, npx flaglint@latest still works exactly as before. The Homebrew path is an addition, not a replacement.

Once installed, the quickstart walks through the full audit → preview → apply workflow.

FlagLint Is Now Listed on the OpenFeature Ecosystem

FlagLint is now listed in the OpenFeature ecosystem directory as one of two integrations in the JavaScript/Server category.

I want to be straightforward about what that means — and what it doesn’t.

The OpenFeature ecosystem is a directory maintained by the OpenFeature project (a CNCF incubating standard) where providers, SDKs, hooks, and integrations can be listed. It is not an award or a certification. It is a discovery page. Teams that are already evaluating OpenFeature — researching providers, looking for tooling, trying to understand the ecosystem — land there.

Being listed means that those teams will now find FlagLint when they filter for integrations. That matters because the people who need FlagLint most are exactly the people who are actively thinking about OpenFeature.

The OpenFeature standard is the reason FlagLint exists. The whole problem FlagLint solves — the argument-order inversion between LaunchDarkly’s boolVariation(key, ctx, default) and OpenFeature’s getBooleanValue(key, default, ctx) — only surfaces when you are trying to move to OpenFeature. If teams weren’t adopting OpenFeature, there would be no migration to get wrong.

So it made sense to be in the directory where those teams are looking. Not to market FlagLint as a product, but to be findable at the point in the journey where someone is asking “what tooling exists around OpenFeature for LaunchDarkly migration?”

What FlagLint does in the context of OpenFeature

Section titled “What FlagLint does in the context of OpenFeature”

FlagLint is not an OpenFeature SDK or provider. It doesn’t evaluate flags. What it does is sit at the boundary between your existing LaunchDarkly codebase and the OpenFeature world you’re moving toward.

Specifically:

  • Audit — inventory every direct LaunchDarkly SDK call, classify each one by migration risk, produce a readiness score
  • Migrate — preview and apply proven-safe call-site rewrites that transpose arguments correctly and rename methods atomically
  • Validate — enforce in CI that no new direct LaunchDarkly calls land once you’ve drawn the boundary

None of that requires a network connection, an API key, or access to your LaunchDarkly environment. It’s all static analysis on your source code, running locally.

If you’re in the process of moving to OpenFeature and want to understand your current exposure before touching any code, the audit command is the right starting point.

Terminal window
npx flaglint@latest audit ./src

It runs in under a minute on most codebases and doesn’t touch any files.