Skip to content

feat: CLI v2#43

Draft
khvn26 wants to merge 87 commits into
mainfrom
feat/cli-v2
Draft

feat: CLI v2#43
khvn26 wants to merge 87 commits into
mainfrom
feat/cli-v2

Conversation

@khvn26

@khvn26 khvn26 commented Jul 21, 2026

Copy link
Copy Markdown
Member

Contributes to #42.

To try it, make sure you've got Go 1.26+ installed and:

git clone -b feat/cli-v2 https://github.com/Flagsmith/flagsmith-cli.git
cd flagsmith-cli
go build -o flagsmith .

./flagsmith init
./flagsmith flag list

khvn26 added 30 commits July 16, 2026 15:56
Remove the TypeScript/oclif CLI entirely and start over in Go + cobra,
per the installation & auth design (docs/design/001-installation-and-auth.md).

The PoC implements interactive auth against the core OAuth 2.1 server
(Flagsmith/flagsmith#8029):

- `flagsmith login`: authorization-code + PKCE on a loopback listener
  (literal 127.0.0.1/[::1], RFC 8252), endpoints discovered via RFC 8414
  metadata, explicit `management-api` scope, `--no-browser` for headless
- credentials in the OS keychain, 0600-file fallback
- `flagsmith auth status` / `auth token`: transparent refresh-token
  rotation (server grace period 120s)
- `flagsmith logout`: server-side revocation + local removal

Verified end-to-end against ghcr.io/flagsmith/flagsmith:pr-8029.

beep boop
Tracks the scope rename in Flagsmith/flagsmith#8029. Verified e2e
against the rebuilt pr-8029 image (metadata now advertises admin-api).

beep boop
- oauth: full PKCE login against an in-process fake authorization server
  (asserts the exact authorization request and token-exchange contract,
  including verifier→challenge), state-mismatch rejection, access_denied,
  context cancellation, exchange failure, discovery errors, refresh
  rotation form, and revocation
- store: keychain round-trip (go-keyring mock), forced file fallback with
  0600 perms, corrupt-file and not-logged-in paths; make keychain removal
  in Delete best-effort so a broken keychain can't fail logout
- cmd: drives login --no-browser → auth status → auth token → logout
  end-to-end in-process, with the test playing the browser

beep boop
Implements the 02-authentication design (bar OIDC), red-green TDD:

- FLAGSMITH_API_KEY resolved ahead of stored logins, classified by shape:
  dotted -> Master API key (Api-Key header), dotless -> bearer token;
  ser.* keys and legacy 40-hex authtokens rejected with targeted errors
- flagsmith login --token / --token-stdin to store a Master API key
  (verified against /organisations/ before saving)
- credential store keyed per instance API URL (keychain account = URL,
  file fallback becomes a URL-keyed map); logout --api targets one
  instance and only revokes OAuth sessions
- auth status shows organisations and the active credential source;
  plaintext file fallback now warns visibly
- auth token prints whichever Admin API credential is active

Verified against staging (real browser PKCE login, refresh-token
rotation, per-instance isolation) and against a local pr-8029 stack
for the master-key paths with a real generated key.

beep boop
`flagsmith auth login` / `flagsmith auth logout` now work identically to
the top-level spellings (gh-style muscle memory). Commands are built by
constructors and registered under both parents; the flow tests are
parametrised over both shapes.

beep boop
…y doc

- `environment` now holds the client-side SDK key (env names have no
  server-side uniqueness constraint; keys are unique and public);
  `environmentKey` removed; schema anyOf(project, environment) so
  SDK-only configs are one field. Names come from a cosmetic name cache
  (os.UserCacheDir), never from the file.
- Sources collapse to flag/env/default; schema fields map 1:1 to flags
  and FLAGSMITH_* env vars; config --json works credless/offline.
- New 02-output-and-interactivity.md for cross-cutting conventions
  (TTY rules, --yes/--no-input/FLAGSMITH_NO_INPUT, --json, exit codes);
  auth and project-config docs renumbered to 03/04.

beep boop
The plaintext file store is no longer a silent fallback. When the OS
keychain is unavailable (headless Linux, containers, SSH), login fails
closed before starting any flow — browser logins probe storage first so
we never mint tokens we can't keep — and the error names both ways out:
FLAGSMITH_API_KEY, or --insecure-storage to opt into the 0600 plaintext
file. Refreshed sessions persist to whichever store they were loaded
from (never migrating between stores), and auth status labels the
opt-in store as "file (plaintext)".

beep boop
Matches the flagsmith.json field name (apiUrl) and the --config-path
convention from the project-config design. The alias is implemented via
cobra's global flag-name normalization, so it works in any position and
never appears in help. Also specs out-of-repo config discovery (cwd
only) and keys-only -e/FLAGSMITH_ENVIRONMENT in 04.

beep boop
…ONMENT (context)

FLAGSMITH_ENVIRONMENT_KEY is the SDK credential — takes precedence and
is the only home for server-side ser.* keys. FLAGSMITH_ENVIRONMENT is
the default-environment context (client-side key, mirroring
flagsmith.json). Also: 04 rewrite cleanups (env-key diff fix, typos,
cache path, out-of-repo discovery, keys-only -e), matrix rows updated
for the removed name-derivation path.

beep boop
Nearest-file discovery walking up to the git toplevel (cwd only outside
a repository), forward-compatible parsing (unknown fields warn, not
fail), and rejection of server-side keys in the environment field with
an error pointing at FLAGSMITH_ENVIRONMENT_KEY.

A schema-drift guard test cross-checks the parser's field list against
schema/flagsmith.json — and immediately caught $schema missing from the
schema's properties, which with additionalProperties:false would make
editors reject the $schema line that flagsmith init writes. Restored.

beep boop
- Global context flags (-p/--project, --organisation, -e/--environment,
  --sdk-api-url, -c/--config-path) with cli > env > config > default
  precedence per value; sdkApiUrl follows a non-default apiUrl, else
  Edge; ser.* keys rejected in context with a pointer at
  FLAGSMITH_ENVIRONMENT_KEY
- flagsmith config shows the resolved context with per-value sources;
  --json is the scripting interface; credless and offline, names are
  best-effort enrichment from the local cache
- name cache at os.UserCacheDir()/flagsmith/cache.json keyed by
  instance; auth status seeds organisation names opportunistically
- auth commands resolve their instance through the full chain, so a
  committed apiUrl finally works without --api-url

beep boop
Implements the rest of 04-project-config:

- flagsmith init: interactive flow (inline browser login when needed,
  org picker for multi-org users with the choice recorded, project
  picker with inline creation defaulting to the cwd name, environment
  picker writing the client-side key) and non-interactive flow
  (--project/--environment/--yes; missing input is a usage error).
  Values from an existing flagsmith.json act as prompt defaults, never
  as decisions; re-init shows a -/+ diff and confirms. The environments
  call doubles as the access check and seeds the name cache. $schema is
  pinned to the writing CLI's version tag.
- Admin API client: Projects, CreateProject, Environments, with
  paginated-or-bare-array list decoding.
- Exit codes per 02: usage errors (a prompt would have collected it)
  exit 2, everything else 1. --yes/--no-input aliases with
  FLAGSMITH_NO_INPUT; prompts require a real TTY.
- Bare `flagsmith` nudges towards init when there is neither project
  context nor credentials.

Verified live against staging (non-interactive init against a real
project). Also fixes a test-harness bug where cobra's sticky --help
local flag leaked across Execute calls.

beep boop
- --json is a global flag with FLAGSMITH_JSON_OUTPUT, now covering auth
  status (identity, organisations, source, expiry) and auth token
  alongside config
- browser rule enforced: without a TTY, login never opens a browser
  (implicit --no-browser); with --yes/--no-input it refuses outright,
  pointing at --token-stdin and FLAGSMITH_API_KEY instead of hanging CI
  for the login timeout
- logout's revoke warning moves to stderr; stream discipline pinned by
  tests with split stdout/stderr capture
- invalid promptable input (e.g. bad FLAGSMITH_PROJECT) is a usage
  error: exit 2, not 1
- fixes the FLASGMITH_JSON_OUTPUT typo in 02

beep boop
Replace the hand-rolled selector with huh: on a real terminal, pickers
are full-terminal selects with arrow keys, j/k, and type-to-/-filter for
long lists (a real win for the org picker); everywhere else huh's
accessible mode gives numbered, line-based, screen-reader-friendly
prompts that re-prompt on invalid input and terminate on EOF (never
hang). Colors come from termenv and respect NO_COLOR.

internal/prompt keeps its Select/Text/Confirm API and the
stdinIsTTY/rawTerminal cmd seam, so the accessible path is exactly what
the command tests drive. Accessible input is fed one byte at a time
because huh spins up a fresh scanner per prompt and a buffered source
would let one prompt swallow the next's input (regression-tested).

Verified live against staging in tmux: arrow-key org selection,
/-filtering the project list, and the ←/→ confirm on re-init. Docs
transcripts in 04 updated to the real huh rendering.

beep boop
Two bugs in how `flagsmith init` treated an existing file's organisation:

- It was dropped entirely: organisationID started at 0 and was only set
  by the multi-org picker, so any invocation that skipped the picker
  (single-org user, or explicit --project) rewrote the file without its
  organisation, showing `- "organisation": N` in the diff. Seed it from
  the resolved context.
- A config-file organisation suppressed the picker, so a multi-org user
  could never change it via re-init. Now only a flag/env organisation
  skips the picker (explicit decision); a config value is just the
  picker's pre-selected default, so re-init always re-offers the choice.

Verified live against staging in tmux: re-init re-offers the org picker
pre-selected to the current org.

beep boop
A project with no environments (typically one just created inline) had
nothing to pick from, so init wrote a config with no environment. Now,
interactively, it prompts to create one (default name Development),
creates it via the new api.CreateEnvironment, and records its key.
Non-interactively an empty project is written without an environment
rather than creating one silently.

The test fake now distinguishes an accessible-but-empty project (200,
empty list) from a no-access project (403), which the access check
relies on. Verified live against staging: a freshly created project
reports [] environments and init creates Development end-to-end.

beep boop
New internal/output package owns the JSON-vs-human decision once:
output.Render(w, data, opts, human) marshals the same data value that
feeds the human view — so the two can't drift — and Table/Detail/Success
give commands a consistent, NO_COLOR-aware, pipe-friendly vocabulary.

- --jq <expr> (global, implies --json) filters JSON output through a jq
  expression via itchyny/gojq: raw for string results, compact JSON
  otherwise. Works on every command that outputs data.
- Result model (02): stdout is the data result; ✓ confirmations,
  progress, warnings and prompts go to stderr. Migrated config, auth
  status (now a Detail view, single UsersMe call) and auth token onto
  Render; moved login/init/logout ✓ lines to stderr so mutations leave
  stdout empty (delete-style contract).
- JSON mirrors the resource shape (bare object/array); config keeps its
  bespoke keyed shape.

Verified live against staging: --jq filtering, migrated rendering, and
the stdout/stderr split.

beep boop
Answers the 02 gap where two init prompts (project name, environment
name) had no flag equivalent and the overwrite-confirm exited 1 instead
of 2.

- Prompt primitives now take the flag that supplies the same value and
  self-guard: called without a TTY they return a usage error (exit 2)
  naming that flag. A prompt can no longer be written without linking a
  flag, and can no longer hang non-interactively — the contract is
  structural, not per-call-site discipline. confirmOrYes centralises the
  yes/no case (--yes answers it; no TTY without it is exit 2 naming --yes).
- --create-project <name> / --create-environment <name> make creation
  flag-drivable, each mutually exclusive (exit 2) with its select
  counterpart; org resolution factored into resolveOrganisation so both
  the create and select paths share it.
- Re-init now carries a config-file environment forward non-interactively
  (it was silently dropped, the same class of bug as the earlier org drop).

Verified live against staging: non-interactive create of project +
environment, mutual-exclusion and overwrite-without-yes both exit 2.

beep boop
Move the result model out of 02 (it is CRUD-shaped, not general output)
into a new 05-crud.md defining the shared shape every resource command
follows: command layout, addressing, result model (human list shows a
count; JSON is a bare array), mutations, errors, and pagination (list
fetches all pages, --limit caps). Also fixes a typo in 02.

beep boop
Scope flags to feature states (the SDK view); project-level feature
definitions are a separate `features` resource, deferred. Resource
definition plus `flags list` as the minimum viable command: reads via
the SDK API with just an environment key (GET /api/v1/flags/), so it
works right after `init` and makes the init nudge real. get and the
enable/disable/set mutations (Admin API, v2-versioning branch) noted
as later work.

Also lands 05-crud.md (CRUD conventions) from the prior step.

beep boop
Implements the minimum from 06-flags.md, making the `flagsmith init`
next-step nudge real. `flags list` reads feature states from the SDK API
(GET /api/v1/flags/, X-Environment-Key) using the environment key from
context — FLAGSMITH_ENVIRONMENT_KEY (may be server-side) or the
client-side key in flagsmith.json — so it works with no Admin API
credentials at all.

Human output is a NAME/ENABLED/VALUE table with a count; --json/--jq
get the bare array as the API returns it (per 05's result model).
Verified live against staging with only a committed environment key.

beep boop
Colour the human tables after tabwriter has aligned them, not before:
Table bolds the header line and Detail colours the label column once the
plain text is laid out, so the ANSI bytes never count toward column
width. Restores the bold header (and cyan detail labels) while keeping
`flags list` and the detail views aligned; piped output stays plain.

beep boop
So a flagsmith.json written by a branch build resolves its schema (main
has no schema/flagsmith.json yet). Revert to main / a release tag before
merge.

beep boop
Point $schema at the feat/cli-v2 ref so it resolves like the files init
now writes. Revert to main / a release tag before merge.

beep boop
Adopt singular resource nouns for CRUD commands (gh/kubectl style):
`flagsmith flag list`, not `flags`. Renames the command and its cobra
identifiers, the init next-step nudge, tests, and the command examples
in 05-crud.md / 06-flags.md.

beep boop
Leftover from the rename to 04-project-config.md; accidentally re-added
in the previous commit by `git add -A`. 03 is authentication; the
project-config doc lives at 04.

beep boop
khvn26 added 19 commits July 22, 2026 19:21
Two write paths omitted a field the Admin API requires in the body,
not just the URL:

- segment create/update returned 400 {"project":["This field is
  required."]} — the serializer requires `project` in the body.
- feature variant update returned 500 (KeyError: 'feature' at
  multivariate/serializers.py:83) — validate() reads attrs["feature"]
  even on a partial PATCH, so an update omitting it is unhandled.

Set the URL-derived id into the body in CreateSegment, UpdateSegment,
and UpdateMVOption. Tests assert each field reaches the wire.

beep boop
Regenerate every example in the implemented-command docs (feature,
segment, flag) from real CLI runs against a seeded demo project, so the
column widths, ID formats, JSON shapes, and error messages match what
the tool actually emits. Leaves 10-projects-organisations.md as-is
(those commands are not implemented yet).

beep boop
`flagsmith project` and `flagsmith organisation` are flat CRUD (05), so a
short doc suffices — with callouts for the wrinkles: gated organisation
create (superuser setting / SAML), project create requiring an organisation
that is then immutable, read-only/plan-gated project fields, and delete
needing object admin.

beep boop
Slice A of 10. `flagsmith organisation` (alias `org`) list/get/create/update/
delete, referenced by name or id. --json mirrors the API's full field set via
a raw-preserving Organisation (rawItem). Adds GetOrganisation/CreateOrganisation
/UpdateOrganisation/DeleteOrganisation; create/update take a flat field body
(--force-2fa, --webhook-email, --name on update).

beep boop
Slice B of 10. `flagsmith project` list/get/create/update/delete, referenced
by name or id. list shows the organisation name (--organisation scopes it,
else all accessible); --json mirrors the API's full field set via a
raw-preserving Project. create uses the resolved organisation (--organisation
context) and it's immutable thereafter; update patches name/settings.

CreateProject moves to a flat-body signature (Projects gains an all-orgs
mode when organisationID is 0); init and the api test updated to match.

beep boop
Now that project/organisation CRUD is implemented, replace the design
draft's illustrative output with format-accurate examples: real column
spacing, "Name (id)" organisation labels in project list, the detail
block create/update print after the confirmation, and the exact count
lines. Organisation data stays curated (Acme/Beta) since the live list
is the account's real orgs; mutation examples are illustrative as those
operate on real billing entities.

beep boop
`flagsmith environment` (alias `env`) CRUD plus clone and server-side SDK
key management — kept separate from projects/organisations (10) because
environments are keyed by their client-side api_key rather than an int id,
create mints that key, and they carry a second (server-side `ser.`) key
type. v2 feature versioning is intentionally not a CLI command (use
`flagsmith api`); the project comes from context.

beep boop
Slice A of 11. `flagsmith environment` (alias `env`) list/get/create/update/
delete/clone, project-scoped, referenced by name or client-side api_key. get/
update/delete/clone address the environment by its api_key (not an int id);
--json mirrors the API's fields via a raw-preserving Environment. create mints
the key with the project taken from context; clone copies into a new named
environment. CreateEnvironment moves to a flat-body signature (init and the
api test updated).

beep boop
Slice B of 11. `flagsmith environment key list|create|delete <environment>`
manages an environment's server-side (ser.) SDK keys. list omits the secret
(NAME/ID/ACTIVE/CREATED/EXPIRES); create prints the minted key once to
stdout with a store-it-now note on stderr; delete is by key id. Adds
EnvironmentAPIKeys/CreateEnvironmentAPIKey/DeleteEnvironmentAPIKey.

beep boop
`flagsmith environment document [environment]` outputs the environment
document (the offline-evaluation payload for server-side SDKs) via the admin
GET .../{api_key}/document/ action — always JSON, composes with --jq, env
from an argument or context.

beep boop
`flagsmith environment document [environment]` outputs the environment
document (the offline-evaluation payload) via the admin GET
.../{api_key}/document/ action — always JSON, composing with --jq. The
environment comes from an argument or, when omitted, the context environment.
Adds EnvironmentDocument to the client.

beep boop
Establishes three conventions: exit-2 (usage) errors print the nearest
command's usage while exit-1 errors print the error alone; every command's
help carries an examples block (command lines, generally no output); and any
error may append an optional hint — a recovery command and/or a context link
(pricing when plan-gated, docs when misused).

beep boop
Implements the "Help and errors" conventions from 02 §4:

- Errors can carry a hint (a recovery command or context link) rendered after
  the message. Attach one explicitly with withHint/hintf, or let hintFor derive
  it automatically from a recognised condition — one central place maps error
  conditions to guidance, so any command gets the hint for free. Wired for
  auth.ErrNotLoggedIn (login), ErrPlanGated (pricing), and ErrWorkflowGated
  (docs); ErrNotLoggedIn's message is now factual, with recovery in the hint.
- Plan/subscription limits surface as api.ErrPlanGated and always suggest the
  pricing page. Detection is central (responseError parses the DRF error body):
  Flagsmith ships no machine-readable error code, so plan limits are matched by
  their detail strings — which also lets every error surface the API's own
  reason instead of a bare status. 403 caps are wire-identical to RBAC denials
  and intentionally left unclassified.
- Incorrect input exits 2 and prints the nearest command's usage: our
  usageErrors, plus cobra's own flag-parse and arg-count failures (routed
  through usageError via FlagErrorFunc and an Args wrapper).

beep boop
Completes the "Help and errors" conventions from 02 §4: every runnable command
now carries an examples block of representative invocations (command lines,
generally no output), sourced from the design docs. TestEveryCommandHasExamples
walks the command tree and fails if any non-hidden runnable command lacks one,
so the convention holds as commands are added.

beep boop
getList decoded only the first page's results and dropped the DRF
"next" link, so large feature/segment/environment/identity lists were
silently truncated and name resolution could report "not found" for
items on page two.

Follow "next" to the end, reusing apiURL with only the link's path +
query so pagination keeps working behind host-rewriting proxies. This
covers page-number endpoints and the edge-identity last_evaluated_key
cursor shape alike; bare-array responses are unchanged.

Replace the envelope-only "paginated response" test with a genuine
two-page test, and add TestEdgeIdentityUUID for the cursor shape.

beep boop
The design promises stdout carries the data result and stderr carries
everything else, including prompts (02-output-and-interactivity §2). But
promptIO wired the prompt UI to cmd.OutOrStdout(), so an interactive
confirmation or selection was written into the data stream — corrupting a
redirected result, e.g. `flagsmith feature delete old --json > result.json`
where the confirmation UI lands in result.json before the JSON.

Encode the contract in the type: prompt.IO's output field is now ErrOut and
the struct has no data writer, so a prompt structurally cannot reach stdout.
promptIO wires it to cmd.ErrOrStderr().

beep boop
Every network path (Admin API, OAuth, raw `api` command) used
http.DefaultClient with no timeout or injectable transport, so a stuck
DNS/TLS/read could hang forever, transport behaviour was untestable, and
there was no home for a versioned User-Agent, retries or Retry-After.

- internal/httpx: the one *http.Client the CLI uses — per-connection
  timeouts, a User-Agent set when absent, and retries for idempotent
  reads only (GET/HEAD on network errors/429/5xx, capped backoff,
  Retry-After honoured, aborts on context cancellation).
- internal/api: Client owns the *http.Client, base URL, auth and
  User-Agent; every api.X(ctx, apiURL, auth, …) is now a (*Client)
  method.
- internal/auth: OAuth paths take an injected *http.Client.
- internal/version: single source of truth for the version tag and the
  derived User-Agent (flagsmith-cli/<version> (<os>/<arch>)).
- internal/cmd: shared client + apiClient helper; commands get a 60s
  overall deadline (FLAGSMITH_TIMEOUT overrides, 0 disables) with
  login/logout exempt.

beep boop
… parse

init rebuilt flagsmith.json from scratch on every run, which lost data and
risked destroying hand-edited files:

- A custom sdkApiUrl was never copied into the rewritten file, so re-running
  init silently dropped a configured SDK endpoint. It is now carried forward
  whenever it was set explicitly (flag, env, or the existing file).
- A malformed flagsmith.json was replaced with an empty config instead of
  stopping — combined with --yes / FLAGSMITH_NO_INPUT this could overwrite a
  broken-but-recoverable file. init now hard-errors and leaves it untouched.
- Writes went straight through os.WriteFile; an interrupted write could
  truncate the file. config.File.Save now writes a temp file and renames it
  into place atomically.
- Unknown fields were discarded on rewrite. Load captures them and Save
  re-emits them, so a newer file edited by an older CLI round-trips intact.

beep boop
--no-input and --yes were one flag: FLAGSMITH_NO_INPUT=1 (a CI-wide
"don't hang" switch) silently authorized every destructive delete.
These are orthogonal concerns:

- --no-input / FLAGSMITH_NO_INPUT: never prompt or open a browser;
  fail (exit 2) if required input is missing.
- --yes: answer confirmations affirmatively. No env var — consent
  should not be ambient across a CI environment.

A confirmation now resolves as: --yes -> proceed; else interactive
TTY -> prompt; else (--no-input, env, or no TTY) -> exit 2 naming
--yes. Non-interactive execution never authorizes on its own.

confirmOrYes is shared, so every destructive command picks this up at
once. Browser login is now gated on --no-input, not --yes.

beep boop

@Zaimwa9 Zaimwa9 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of comments

Comment thread internal/cmd/init.go
Comment on lines +346 to +362
if _, err := os.Stat(target); err == nil {
old, _, loadErr := config.Load(target)
if loadErr != nil {
old = &config.File{}
}
if stdinIsTTY() && !yesFlag {
fmt.Fprintf(out, "%s exists — updating it.\n\n%s\n", config.FileName, fileDiff(old, newFile))
}
ok, err := confirmOrYes(cmd, "Write changes?")
if err != nil {
return err
}
if !ok {
fmt.Fprintln(errOut, "Aborted; nothing written.")
return nil
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO we can skip this confirmation part and let the switch happen directly. I'm not really seeing the extra value of showing a diff of non-human readable string.

Let's trust the process!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After having fixed a case where sdkApiUrl would get silently overwritten, I'd like to keep it for the time being.

Let's trust the process!

I'd argue a short diff output gives the user more confidence than silently writing stuff under the hood.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can continue this over at the RFC Project Config section.

Comment thread internal/cmd/init.go Outdated
)

var initCmd = &cobra.Command{
Use: "init",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd propose a renaming here. I got really confused as init rings like bootstrap once and forget to me.

I would propose link which sounds more like the industry standard to my ear

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After playing around. I double down on having a link command so init could be used to set the API / SDK API / config file path instead (and under the hood default to a project/organisation/environment combination)

@khvn26 khvn26 Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd like to push back here.

I deliberately scoped flagsmith.json to a single project dir to avoid having user/machine-scoped configuration file at all, at least for the time being. This keeps context composition relatively simple. I thought on the flagsmith.json schema carefully, and I can argue that each key easily belongs in the project-local config file. Individual keys are still overridable via CLI flags and environment variables.

"Bootstrap once and forget" is precisely the primary flow I targeted when designing this. I regard re-init use case as supplemental (it is, however, very well supported).

I think decoupling project init from CLI bootstrap is bad UX. A user should be confident the CLI is fully ready to use once flagsmith init is done. I see little value in having a separate command that only does part of the job.

For reference, my inspirations were gcloud init, and depot init.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I welcome further thoughts/comments on project config in the relevant RFC section.

Comment thread internal/cmd/api.go Outdated
Use: "api <path>",
Short: "Call the Flagsmith API with the CLI's credentials",
Example: ` # GET any endpoint with the CLI's credentials applied
flagsmith api /organisations/

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All those examples are wrong for a normal saas users. It gives a 404, should be api/v1/organisations

not sure how to deal with self-hosted

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hot thinking. I don't remember as of whether self-hosters have the api/v1 prefix. If yes, i'd add it to the exemple. If not, shouldn't we default apiUrl=https://api.flagsmith.com/api/v1 and not hardcode the prefix in every call ?

@khvn26 khvn26 Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good spot, thanks! Examples fixed in 52b689a.

Regarding the prefix, since we're giving the CLI full API access, including /.well-known, /oauth, /o, /api/experimental and so on, IMO we shouldn't limit flagsmith api to v1 as well.

I believe the API traffic is served under the same /api/v1 prefix in single-container self-hosted deployments.

Comment thread internal/cmd/api.go Outdated
flagsmith api /organisations/

# typed fields build a JSON body; -X sets the method
flagsmith api /projects/ -X POST -F name="Acme" -F organisation=13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now to use the SDK evaluation we need:
./flagsmith api /api/v1/flags/ --sdk

I'd like to propose to create a sdk command directly that would use the sdk url under the hood directly. It's also more explicit and reduce the cognitive load

This way we could have an interface like this

 # evaluate the environment's flags (what an anonymous client receives)
flagsmith sdk flags
flagsmith sdk flags --json
flagsmith sdk flags -e Ast8aAES9zpaDqNxMdKUWo   # any other env, one-off

# evaluate for an identity (segment rules + multivariate splits applied)
flagsmith sdk identify test-user-123
flagsmith sdk identify test-user-123 --trait plan=enterprise --trait beta=true

# raw escape hatch for anything else — `api` then becomes admin-only
flagsmith sdk api /api/v1/identities/?identifier=test-user-123

Exemples generated with claude but you get the spirit!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Design for flagsmith eval added in d85620c. Haven't implemented the PoC yet but eager to read your thoughts over at the Notion doc.

khvn26 added 6 commits July 23, 2026 11:34
…ACCESS_TOKEN

FLAGSMITH_API_KEY previously accepted either a Master API key or a bearer
token, with the CLI guessing the Authorization scheme from token shape. The
only reason for the bearer path was CI OIDC-exchanged tokens.

Give OIDC-exchanged bearers their own variable, FLAGSMITH_ACCESS_TOKEN, so
each Admin API env var maps to exactly one credential kind and scheme. The
shape classifier (ClassifyAPIKey) collapses into a ValidateMasterKey guard
that only turns the common paste-mistakes into actionable errors; classify.go
is renamed kind.go to match what it now holds.

beep boop
segment update lacked the "nothing to update" guard its sibling
commands have, so a no-flag invocation fetched the segment and
re-PUT it unchanged — an audit-log entry and rule-tree overwrite
for a no-op. Fail fast before any network call instead.

beep boop
Wrap the base transport so every request — retries included — logs
method, URL, status, total duration, and TTFB. Never logs headers or
bodies, keeping the Authorization credential out of the trace. Enabled
via FLAGSMITH_DEBUG, matching the CLI's FLAGSMITH_* env convention.

beep boop
@khvn26 khvn26 mentioned this pull request Jul 23, 2026
5 tasks
khvn26 added 3 commits July 23, 2026 16:47
Based on Flagsmith/flagsmith#7955 and #8000: repeatable --weight k=n on
flag update (env default and per segment), merge semantics over the
absolute MV list, and a Variants block in flag get.

beep boop
`flagsmith evaluate` (alias `eval`) shows what a Flagsmith SDK would return for
the current environment — the resolved flags at runtime, optionally for an
identity and traits. SDK surface (environment key, no Admin login), a read-only
what-if by default (transient), with --persist to force remote persistence.

beep boop
The `api` command passes the path through verbatim, so the example paths
without an `api/v1/` prefix 404 against SaaS (and self-hosted, which shares the
prefix — only the host differs, via --api-url). Match the design doc's path
style, and split the redundant `-X POST -F` example: -F already implies POST,
so demonstrate -X where it matters (a bodiless DELETE).

Addresses PR review feedback.

beep boop
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants