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.
avsb codegen --project 42Flags
| Flag | Required | Default | Meaning |
|---|---|---|---|
-p, --project <project> | No | the local manifest | The project: PRJ-42, 42, or a dashboard URL |
-e, --env <key> | No | production | Environment key to read the datafile from |
-o, --output <path> | No | ./src/generated/flags.ts | File to write. Parent folders are created |
-m, --manifest <path> | No | not written | Also write a JSON manifest of the same flags |
--no-augment | No | off | Leave out the AvsbFlags block, so flag keys stay plain strings |
--check | No | off | Write nothing; compare with what is on disk and exit non-zero on drift |
--watch | No | off | Keep regenerating while the platform changes |
--interval <seconds> | No | 10 | How often --watch polls (2 to 300) |
--json | No | off | Print the result as one JSON document |
--quiet | No | off | Print 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 examplehttps://app.avsb.cloud/projects/42/dashboard. - It is also shown as
PRJ-42on the project card in the projects list.
avsb codegen --project 42avsb codegen --project PRJ-42avsb codegen --project https://app.avsb.cloud/projects/42/dashboardInside 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.
cd project-42avsb codegen # same as --project 42Anything 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:
avsb codegen --project 42 --env development --output src/generated/flags.dev.tsWhat it writes
Given a boolean flag, a string flag with two variations, a JSON flag, and a registered plan attribute:
// 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 } }}Rules the emitter follows:
FlagKeylists every flag key in the datafile, alphabetically. A project with no flags emitsexport type FlagKey = never.- Boolean and number flags become
booleanandnumber. - 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 tostring. - JSON flags are inferred from the values their variations carry (see below).
ContextSchemahas one entry per context kind, each withkey: stringplus every registered attribute for that kind. Attribute types follow their declared type; JSON attributes becomeunknown. Attributes with no declared kind belong touser.- The
AvsbFlagsblock 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
stringrather 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.
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)The value types come along too, which is what makes JSON flags pleasant:
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,})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:
| Package | Where the key is typed |
|---|---|
@avsbhq/browser | every typed getter, plus getSnapshot |
@avsbhq/react | useFlag, useBoolFlag, useStringFlag, useNumberFlag, useJsonFlag, useFlagValue, useFlagSuspense, useExposure |
@avsbhq/next | the React hooks, plus evaluateFlagServer(datafile, context, 'key', fallback) |
@avsbhq/vue | every composable, through the shared FlagKeyInput type |
@avsbhq/svelte | the flag stores, the *State runes, subscribeFlag, and the SvelteKit bound client |
@avsbhq/solid | createFlag and friends, through the shared FlagKeyInput type |
@avsbhq/angular | the avsb*Signal helpers, the avsb*$ observables, both pipes, and both directives |
@avsbhq/react-native | the 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 asRef<AvsbFlagKey>. Solid'sAccessor<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), importingAvsbFlagKeyfrom@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.
avsb codegen --project 42 --check✗ 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.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.
- name: Check flag types are current env: AVSB_TOKEN: ${{ secrets.AVSB_TOKEN }} run: | npm install -g @avsbhq/cli avsb codegen --project 42 --checkWith --json the same run is machine-readable and keeps the same exit code:
{ "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" }}Watching while you work
--watch regenerates whenever the platform changes, so a flag you create in the dashboard reaches your editor without switching windows.
avsb codegen --project 42 --watchavsb codegen --project 42 --watch --interval 30What 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.
avsb codegen --project 42 --manifest src/generated/flags.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"] } ]}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
publishedAtis the environment's last publish, which--checkleaves out of the comparison), so--checkcompares it exactly the way it compares the.tsfile and a manifest committed alongside the types is guarded too. enabledanddefaultVariationarenullrather than guessed when the datafile does not carry them. "We do not know" is a fact your script can branch on.environmentsis an array at both levels so a future multi-environment run can add entries without changing the contract. Today one run reads one environment.versionchanges 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.
avsb find-refsavsb find-refs --project 42 --dir apps/webavsb find-refs --strict| Flag | Default | Meaning |
|---|---|---|
-p, --project <project> | the local manifest | Which project's flags to compare against |
-d, --dir <path> | . | Directory to scan |
-i, --ignore <pattern> | none | gitignore-style pattern to skip. Repeatable |
--max-files <count> | 5000 | Stop after this many files |
--strict | off | Exit non-zero when code references a flag the platform cannot serve |
--json | off | Print the result as one JSON document |
--quiet | off | Print the result only |
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: 6How 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
.gitignorefiles, nested ones included, plus a fixed list of directories that are never source (node_modules,dist,.next,coverage, and friends).--ignoreadds 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 codegenare 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:
- name: Generate flag types env: AVSB_TOKEN: ${{ secrets.AVSB_TOKEN }} run: | npm install -g @avsbhq/cli avsb codegen --project 42 --output src/generated/flags.tsSee CLI Authentication for the full precedence rules.
Convenient local scripts:
{ "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" }}With --json, a normal run reports what it wrote, which project it read, and where that project came from:
{ "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" }}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
--envwhen you need both. - The scan is text, not a parser.
avsb find-refsmatches 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-refsreads the flag list instead, so it still works.
Related
- Typed Contexts explains context kinds and registered attributes.
- SDK Installation covers evaluating flags once the types exist.