CLI Codegen

avsb codegen reads a published datafile (the file your SDK downloads that lists every live flag and its variations) and writes a TypeScript file describing your flags: their keys, their value types, and the shape of the context you evaluate them against. Point your editor at that file and a typo in a flag key becomes a compile error instead of a silent false.

Shell
avsb codegen --project 42
Shell1 line

Flags

FlagRequiredDefaultMeaning
-p, --project <project>Nothe local manifestThe project: PRJ-42, 42, or a dashboard URL
-e, --env <key>NoproductionEnvironment key to read the datafile from
-o, --output <path>No./src/generated/flags.tsFile to write. Parent folders are created
-m, --manifest <path>Nonot writtenAlso write a JSON manifest of the same flags
--no-augmentNooffLeave out the AvsbFlags block, so flag keys stay plain strings
--checkNooffWrite nothing; compare with what is on disk and exit non-zero on drift
--watchNooffKeep regenerating while the platform changes
--interval <seconds>No10How often --watch polls (2 to 300)
--jsonNooffPrint the result as one JSON document
--quietNooffPrint the result only

--check and --watch are mutually exclusive, and --watch refuses --json: a command that never exits cannot print one closing document.

Naming the project

--project takes the project short id in any form the CLI prints it: PRJ-42, 42, or the dashboard URL it appears in.

  • It is the number after /projects/ in the dashboard URL, for example https://app.avsb.cloud/projects/42/dashboard.
  • It is also shown as PRJ-42 on the project card in the projects list.
Shell
avsb codegen --project 42avsb codegen --project PRJ-42avsb codegen --project https://app.avsb.cloud/projects/42/dashboard
Shell3 lines

Inside a folder created by avsb project pull or avsb clone, you can leave --project out entirely: the CLI reads it from .avsb-project.json, then .avsb.json, and prints which file it used.

Shell
cd project-42avsb codegen              # same as --project 42
Shell2 lines

Anything it cannot parse fails immediately with a message naming both places to find the right value.

Choosing an environment

--env takes the environment key (production and development exist by default), and reads that environment's published datafile. Generate types from the environment whose flag set your code targets, usually production:

Shell
avsb codegen --project 42 --env development --output src/generated/flags.dev.ts
Shell1 line

What it writes

Given a boolean flag, a string flag with two variations, a JSON flag, and a registered plan attribute:

Plain text
// AUTO-GENERATED by @avsbhq/cli codegen. Do not edit by hand.// Source: project=42 env=production publishedAt=2026-07-30T09:14:02.000Zexport type FlagKey =  | 'checkout_redesign'  | 'hero_copy'  | 'pricing_table'export interface FlagValues {  'checkout_redesign': boolean  'hero_copy': 'control' | 'variant-a'  'pricing_table': { perks: string[]; tiers: number }}export interface ContextSchema {  'user': { key: string; 'plan': string }}// Teaches every typed getter in @avsbhq/browser and the framework adapters// which keys this project has. Remove this block and they accept any string.declare global {  interface AvsbFlags {    'checkout_redesign': boolean    'hero_copy': 'control' | 'variant-a'    'pricing_table': { perks: string[]; tiers: number }  }}
Plain text27 lines

Rules the emitter follows:

  • FlagKey lists every flag key in the datafile, alphabetically. A project with no flags emits export type FlagKey = never.
  • Boolean and number flags become boolean and number.
  • String flags become a union of their variation values, deduplicated and sorted ('control' | 'variant-a'). If any variation is not a string, or the flag has no variations, the type falls back to string.
  • JSON flags are inferred from the values their variations carry (see below).
  • ContextSchema has one entry per context kind, each with key: string plus every registered attribute for that kind. Attribute types follow their declared type; JSON attributes become unknown. Attributes with no declared kind belong to user.
  • The AvsbFlags block is what makes the SDKs speak your keys. It is written unless you pass --no-augment.

Output is deterministic: the same datafile always produces the same bytes, whatever order the dashboard happens to list your flags in. That is what makes --check trustworthy.

publishedAt in the header is when the environment was last published, not when you ran the command, so running codegen twice in a row writes the same file. An environment that has never been published shows publishedAt=unknown.

The file is regenerated wholesale on every run. Commit it or generate it in CI, but do not edit it: the next run overwrites your changes.

How JSON flags are typed

A flag carries no declared JSON schema on the platform, so the variations you saved are the schema. The emitter merges them:

  • Every variation is an object with the same fields, so you get those fields.
  • A field only some variations carry becomes optional (perks?: string[]).
  • A field whose type differs between variations becomes a union (tiers: number | string).
  • Strings inside JSON stay string rather than becoming literal unions, so adding a third value later does not break code that never needed to change.
  • Variations that disagree on shape entirely (one object, one array) become a union of both.
  • Anything too deep or too wide to render usefully falls back to unknown, and so does a flag with no variations. Narrow it with a type guard of your own when that happens.

Typed flag keys across the SDKs

The AvsbFlags block is a global interface that @avsbhq/core declares empty and your generated file fills in. Every typed getter in @avsbhq/browser and in all seven framework adapters types its key parameter against it:

  • No generated file: the interface has no members, the key type is exactly string, and every call you have written today keeps compiling unchanged.
  • Generated file present: the key type is the union of your real keys, so the editor completes them and a misspelling fails the build.

Import the generated file once anywhere in your program, or list it in your tsconfig.json include, and the whole program sees it.

TypeScript
import type { AvsbClient } from '@avsbhq/browser'declare const client: AvsbClient// Completed from your generated file. A misspelled key here would not compile.const checkout = client.getBoolFlag('checkout_redesign', false)
TypeScript6 lines

The value types come along too, which is what makes JSON flags pleasant:

TypeScript
import type { AvsbClient } from '@avsbhq/browser'import type { FlagValues } from './generated/flags'declare const client: AvsbClientconst pricing = client.getJsonFlag<FlagValues['pricing_table']>('pricing_table', {  perks: [],  tiers: 3,})
TypeScript9 lines

Per framework

Each adapter's README carries the full example in its own idiom. The call you type does not change; only the key parameter got stricter:

PackageWhere the key is typed
@avsbhq/browserevery typed getter, plus getSnapshot
@avsbhq/reactuseFlag, useBoolFlag, useStringFlag, useNumberFlag, useJsonFlag, useFlagValue, useFlagSuspense, useExposure
@avsbhq/nextthe React hooks, plus evaluateFlagServer(datafile, context, 'key', fallback)
@avsbhq/vueevery composable, through the shared FlagKeyInput type
@avsbhq/sveltethe flag stores, the *State runes, subscribeFlag, and the SvelteKit bound client
@avsbhq/solidcreateFlag and friends, through the shared FlagKeyInput type
@avsbhq/angularthe avsb*Signal helpers, the avsb*$ observables, both pipes, and both directives
@avsbhq/react-nativethe React hooks it re-exports

Two consequences worth planning for:

  • Reactive keys narrow too. In Vue a Ref<string> no longer satisfies a composable once keys are generated; declare it as Ref<AvsbFlagKey>. Solid's Accessor<string> is the same story, and so is an Angular template binding that passes a computed string to [avsbFlag].
  • A key computed at runtime needs a cast. client.getBoolFlag(key as AvsbFlagKey, false), importing AvsbFlagKey from @avsbhq/core. It is deliberately a visible, greppable opt-out rather than a hole in the contract.

If a single TypeScript program contains two AvsB projects, generate with --no-augment: two tables would merge into one contradictory global. The named exports (FlagKey, FlagValues) still work, per project, without it.

Checking in CI

--check regenerates in memory, compares against the committed file, and exits non-zero when they differ. Nothing is written. This is the mode that catches the expensive failure: a flag renamed on the platform weeks ago, with the old key still in your code, quietly serving the fallback to real users.

Shell
avsb codegen --project 42 --check
Shell1 line
Plain text
✗ 1 generated file is out of date.  /repo/src/generated/flags.ts is out of date (first difference on line 7).    generated:   'checkout_redesign': boolean    on disk:     'checkout_v2': boolean  Run the same command without --check, then commit the result.
Plain text5 lines

Exit codes follow the usual table: 0 when everything matches, 1 when a file is missing or has drifted, and the auth or usage codes when the run never got as far as comparing. Line endings are normalised before comparing, so a Windows checkout does not fail on \r\n alone. The publishedAt stamp is left out of the comparison too: publishing the environment again with the same flags does not fail the build, while any change to a key or a type still does.

YAML
- name: Check flag types are current  env:    AVSB_TOKEN: ${{ secrets.AVSB_TOKEN }}  run: |    npm install -g @avsbhq/cli    avsb codegen --project 42 --check
YAML6 lines

With --json the same run is machine-readable and keeps the same exit code:

JSON
{  "ok": true,  "command": "codegen",  "data": {    "upToDate": false,    "checked": ["/repo/src/generated/flags.ts"],    "drifted": [      {        "path": "/repo/src/generated/flags.ts",        "reason": "different",        "line": 7,        "expectedLine": "  'checkout_redesign': boolean",        "actualLine": "  'checkout_v2': boolean",        "expectedLineCount": 24,        "actualLineCount": 24      }    ],    "environment": "production",    "flagCount": 3,    "publishedAt": "2026-07-30T09:14:02.000Z"  }}
JSON22 lines

Watching while you work

--watch regenerates whenever the platform changes, so a flag you create in the dashboard reaches your editor without switching windows.

Shell
avsb codegen --project 42 --watchavsb codegen --project 42 --watch --interval 30
Shell2 lines

What it does, and what it deliberately does not:

  • It polls the datafile endpoint. There is nothing local to watch: flags change in a browser tab, not on your disk.
  • Each poll sends the previous ETag, so an unchanged datafile is cheap the moment the endpoint answers conditionally.
  • Files are only rewritten when their contents actually change, so your dev server does not reload on every poll.
  • Unchanged polls print nothing. A regeneration prints the files it wrote.
  • A failed poll is reported and retried with a widening delay, up to a minute apart. Five failures in a row stop the command with a non-zero exit.
  • Ctrl+C stops it immediately and exits 0.

The interval floor is two seconds and the default is ten. The datafile is cached for 60 seconds at the edge, so polling faster than that shows you nothing sooner.

The flag manifest

--manifest writes the same facts as JSON, for the tooling that cannot read TypeScript: a deploy step asserting a flag exists, a script listing what is still serving its default, a weekly count per environment.

Shell
avsb codegen --project 42 --manifest src/generated/flags.json
Shell1 line
JSON
{  "version": 1,  "project": { "shortId": 42 },  "environments": [    { "key": "production", "publishedAt": "2026-07-30T09:14:02.000Z" }  ],  "flags": [    {      "key": "checkout_redesign",      "type": "boolean",      "tsType": "boolean",      "enabled": true,      "defaultVariation": { "key": "off", "value": false },      "variations": [        { "key": "on", "value": true },        { "key": "off", "value": false }      ],      "environments": ["production"]    }  ]}
JSON21 lines

Notes for anyone building on it:

  • The manifest is CI-consumable on purpose. It contains no timestamp of its own, no CLI version, and nothing else that changes between runs (its publishedAt is the environment's last publish, which --check leaves out of the comparison), so --check compares it exactly the way it compares the .ts file and a manifest committed alongside the types is guarded too.
  • enabled and defaultVariation are null rather than guessed when the datafile does not carry them. "We do not know" is a fact your script can branch on.
  • environments is an array at both levels so a future multi-environment run can add entries without changing the contract. Today one run reads one environment.
  • version changes only when a field is removed or changes meaning. New fields are added without a bump.

Finding extinct flags

avsb find-refs scans your source for flag keys and compares them with the platform. It answers two questions in one pass:

  • Referenced but not servable. Code asks for a flag that was renamed, deleted, or archived. The SDK serves your fallback forever and nothing complains, so this stays invisible until someone reads a flat results chart.
  • Never referenced. A flag exists and no code reads it. That is your deletion list, and the only way a flag set stops growing.
Shell
avsb find-refsavsb find-refs --project 42 --dir apps/webavsb find-refs --strict
Shell3 lines
FlagDefaultMeaning
-p, --project <project>the local manifestWhich project's flags to compare against
-d, --dir <path>.Directory to scan
-i, --ignore <pattern>nonegitignore-style pattern to skip. Repeatable
--max-files <count>5000Stop after this many files
--strictoffExit non-zero when code references a flag the platform cannot serve
--jsonoffPrint the result as one JSON document
--quietoffPrint the result only
Plain text
Flag references in /repo  Compared against project PRJ-42 (from .avsb-project.json)✗ 1 flag key referenced but not servable:  checkout_v2 (not on the platform)    src/checkout/Page.tsx:41    src/checkout/hooks.ts:12i 2 flags no scanned file references:  legacy_banner (paused, marked stale)  pricing_table (draft)  Files scanned: 214  Flags in use: 6
Plain text14 lines

How the scan decides:

  • It reads source files by extension (TypeScript, JavaScript, Vue, Svelte, Astro, HTML, and the languages the server SDKs cover) and skips everything else.
  • It honours your .gitignore files, nested ones included, plus a fixed list of directories that are never source (node_modules, dist, .next, coverage, and friends). --ignore adds patterns of your own.
  • A key counts as referenced when it appears as the key argument of a known SDK call (getBoolFlag, useFlag, createJsonFlag, avsbBoolFlagSignal, and the rest), or when the key string appears anywhere at all in a scanned file. The second rule is what stops a flag read through a config map or an HTML attribute from being reported as dead.
  • Files generated by avsb codegen are skipped, so the generated table does not count as a reference to every flag.
  • It compares against the full flag list, not a published datafile, so a flag created this morning and not yet published is never reported as extinct.
  • Symlinks are not followed, and the walk stops at --max-files. A truncated scan says so, because the "never referenced" half is only trustworthy when the whole tree was read.

The exit code is 0 unless you pass --strict. The report is identical either way, so you can start by reading it and turn it into a gate later.

Running it in CI

avsb codegen needs a token. Set AVSB_TOKEN and no avsb login step is needed:

YAML
- name: Generate flag types  env:    AVSB_TOKEN: ${{ secrets.AVSB_TOKEN }}  run: |    npm install -g @avsbhq/cli    avsb codegen --project 42 --output src/generated/flags.ts
YAML6 lines

See CLI Authentication for the full precedence rules.

Convenient local scripts:

JSON
{  "scripts": {    "flags:types": "avsb codegen --project 42 --output src/generated/flags.ts",    "flags:watch": "avsb codegen --project 42 --watch",    "flags:check": "avsb codegen --project 42 --check",    "flags:refs": "avsb find-refs --project 42"  }}
JSON8 lines

With --json, a normal run reports what it wrote, which project it read, and where that project came from:

JSON
{  "ok": true,  "command": "codegen",  "data": {    "output": "/repo/src/generated/flags.ts",    "manifest": null,    "written": ["/repo/src/generated/flags.ts"],    "augmented": true,    "projectShortId": 42,    "projectSource": "project-manifest",    "environment": "production",    "flagCount": 3,    "publishedAt": "2026-07-30T09:14:02.000Z"  }}
JSON15 lines

written lists only the files whose contents changed, so a second run in a row reports an empty array.

Current limits

Worth knowing before you build a workflow on it:

  • TypeScript output only. There is no JavaScript or JSDoc emitter.
  • One environment per run. Generate a second file with a second --env when you need both.
  • The scan is text, not a parser. avsb find-refs matches SDK call names and quoted keys. A key assembled at runtime is invisible to it, in either direction.
  • A published datafile is required for the types. If the environment has never been published, or your plan does not include feature flags, the datafile arrives with no flags and the generated file declares FlagKey = never. avsb find-refs reads the flag list instead, so it still works.
Was this helpful?