CLIBeta
An optional command line for typed flag keys, publishing from CI, and snapshots to bundle into a build. Nothing in the SDK needs it.
The CLI is in beta. Commands, options, and the shape of the generated file may change before 1.0, so pin an exact version in CI, such as @klor/cli@0.1.0-beta.0, rather than tracking @beta. Tell us what is missing or wrong at support@klor.dev.
Running it
Run it with npx @klor/cli@beta, or add it as a dev dependency with npm install --save-dev @klor/cli@beta if a script or CI job uses it, and the command is klor. Spell out the package with npx: an unrelated package on npm is called klor. It reads the key from KLOR_API_KEY, or from --key.
export KLOR_API_KEY=klor_pub_…
npx @klor/cli@beta whoami
# Public key for the production environment, serving #42.Which key a command needs depends on what it does:
types, pull, flags, whoamiany keyA public or secret key reads the published snapshot from the read API, on every plan. A management key also sees flags that are not published yet.
diff, publish, rollback, snapshotsklor_adm_These change or inspect configuration through the management API, which is part of the Pro and Team plans.
Typed flag keys
klor types writes a declaration file listing your project's flags and their types. With it in your project, keys autocomplete, a misspelled key fails the build, and a fallback of the wrong type is refused. Without it, nothing changes: every key is a string, as it always was.
npx @klor/cli@beta types
# Wrote 12 flags to src/klor-flags.d.ts.const checkout = useFlag('checkout_v2', false) // boolean
const limit = useFlag('max_items', 25) // number
useFlag('chekout_v2', false)
// Argument of type '"chekout_v2"' is not assignable to parameter of type …The file goes in src/ if you have one and at the project root otherwise; --out puts it anywhere your tsconfig.json includes. Commit it, and run the command again after adding a flag. It needs @klor/react 0.4 or later. To stop using typed keys, delete the file.
JSON flags take their type from the fallback you pass, because only your app knows the shape it expects. The same types apply to useFlagDetail and to getFlag on the server.
A helper that wraps useFlag with a plain string key stops compiling once keys are typed, because any string is no longer a valid key. Type it the way the SDK does and it works with or without the file, and passes the checks through to its callers:
import { useFlag } from '@klor/react'
import type { FlagFallback, FlagKey, FlagResult } from '@klor/react'
// The fallback's type first, so useAppFlag<Theme>(...) works as useFlag<Theme>(...) does.
export function useAppFlag<T extends FlagFallback<K>, K extends FlagKey = FlagKey>(
key: K,
fallback: T,
): FlagResult<K, T> {
return useFlag(key, fallback)
}By default only keys in the file compile, which is what catches typos. If you would rather write the code for a new flag before regenerating, use klor types --loose: known keys keep their types and autocomplete, and any other string still compiles.
A public key cannot see flags marked sensitive, so a file written with one leaves them out, and an app typed from it cannot compile a read of a sensitive flag. That is the right answer for a client, where such a read only ever returns its fallback; generate with a secret or management key for server code.
To keep the file current in CI, check it rather than rewrite it, with the same kind of key the file was written with. Formatting is ignored, so Prettier, Biome, and Windows line endings do not make it fail; only added, removed, renamed, or retyped flags do.
npx @klor/cli@beta types --check
# src/klor-flags.d.ts is out of date. Run `klor types` to update it. (exit 1)TypeScript 7.0's incremental builds do not re-check a project after any global declaration file changes, this one included, until the build info is cleared. If you build incrementally with TypeScript 7, keep a full tsc --noEmit in CI. TypeScript 5 and 6, tsc -b, and next build pick up a regenerated file on their own.
Publishing from CI
A management key is scoped to one environment, so a pipeline holds one per environment it publishes to. Every command names the project and environment it is about to touch.
# Fail a build that left edits unpublished
npx @klor/cli@beta diff --exit-code
# Publish, with a note for the history
npx @klor/cli@beta publish --yes -m "Release 4.2"
# Serve an earlier snapshot again
npx @klor/cli@beta snapshots
npx @klor/cli@beta rollback 41 --yespublish and rollback show what they will do and ask first. Outside a terminal they refuse unless you pass --yes, so a script never publishes on a guess. publish does nothing when there is nothing to publish.
Bundling a snapshot
klor pull downloads the published snapshot, so a build can ship with real values for a first launch with no network. Pass it as initialSnapshot; the SDK replaces it with a fresher one as soon as it can.
npx @klor/cli@beta pull -o src/klor.snapshot.jsonimport type { Snapshot } from '@klor/react'
import snapshot from './klor.snapshot.json'
const klor = createKlorClient({
apiKey: 'klor_pub_…',
// A JSON import widens literal fields, so TypeScript needs telling.
initialSnapshot: snapshot as Snapshot,
})It downloads the public snapshot unless you pass --private. A private snapshot includes flags marked sensitive and belongs on a server, never in an app.
Options
--keystringAPI key. Defaults to KLOR_API_KEY.
--jsonbooleanMachine-readable output.
-o, --outpathWhere types and pull write their file.
--checkbooleantypes: exit 1 if the file is missing or out of date, and write nothing.
--loosebooleantypes: also accept keys that are not in the file.
--privatebooleanpull: the private snapshot, sensitive flags included.
-m, --notestringpublish: a note for the history.
-y, --yesbooleanpublish, rollback: do not ask for confirmation.
--exit-codebooleandiff: exit 1 if there is anything to publish.
--api-url, --base-urlurlThe management and read APIs. Default to KLOR_API_URL and KLOR_BASE_URL, then https://klor.dev and https://edge.klor.dev.