Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
d75ec06
chore!: remove legacy CLI implementation
flemzord Oct 8, 2026
9ebcffd
feat: Clean & init v4
flemzord Oct 8, 2026
49f6f33
feat: bootstrap minimal fctl v4 foundation
flemzord Oct 8, 2026
553c466
fix(release): migrate Homebrew packaging to casks
flemzord Oct 8, 2026
26ca1fc
feat: Update golangci.yml
flemzord Oct 8, 2026
058aeb0
feat: add Auth and Ledger v4 modules with shared connections
flemzord Oct 8, 2026
1b4e72b
docs: document v4 connections modules and validation
flemzord Oct 8, 2026
63cc9c9
refactor: decouple Auth and Ledger behind public plugin SDK
flemzord Oct 8, 2026
b22dee2
docs: describe embedded plugin contract and validation
flemzord Oct 8, 2026
3812a78
feat: restore default Cloud login and HTTP debug tracing
flemzord Oct 8, 2026
32fc2a8
docs: describe default Cloud login and HTTP diagnostics
flemzord Oct 8, 2026
070908e
feat: restore Cloud commands and styled v4 output
flemzord Oct 8, 2026
86ff1e3
docs: document Cloud CLI and sandbox validation
flemzord Oct 8, 2026
72f7291
docs: clarify deployment validation limits
flemzord Oct 8, 2026
b56de55
fix: simplify human CLI output
flemzord Oct 8, 2026
54de866
feat: add declarative interactive plugin input
flemzord Oct 8, 2026
9f25b0b
fix(plugin): validate choice source exclusion fields
flemzord Oct 8, 2026
1975ed7
fix(plugin): validate choice source match fields
flemzord Oct 8, 2026
6baa4a7
fix(plugin): validate choice source match value lists
flemzord Oct 8, 2026
b0bf2e1
fix: filter selections and validate interactive forms
flemzord Oct 8, 2026
40aaa9f
feat: add guided Cloud Auth and Ledger commands
flemzord Oct 8, 2026
1f4ee5d
docs: record interactive CLI usage and validation
flemzord Oct 8, 2026
97a9b83
build: distribute the plugin development skill
flemzord Oct 9, 2026
4edc454
fix(skills): enable repository discovery in Codex
flemzord Oct 9, 2026
196da42
feat(pluginsdk): support nested resource choices and merge patches
flemzord Oct 9, 2026
ee3391a
feat(connectivity): add catalogue and instance management plugin
flemzord Oct 9, 2026
49c8900
feat(connectivity): merge plugin into stack v4 CLI
flemzord Oct 9, 2026
ee0efb8
refactor(cli): restore profiles naming
flemzord Oct 9, 2026
8d92228
feat(pluginsdk): add isolated gRPC plugin runtime
flemzord Oct 9, 2026
5b0c40e
build(pluginsdk): validate the independent SDK module
flemzord Oct 9, 2026
a5cfb89
feat(plugins): load versioned Ledger executables from OCI
flemzord Oct 9, 2026
ea3b557
fix(plugins): preserve resolved targets and offline metadata
flemzord Oct 9, 2026
7ae5816
refactor(ledger): consume the product-owned plugin
flemzord Oct 9, 2026
ba501a5
fix(cli): style all help command groups
flemzord Oct 9, 2026
cad4121
fix(cli): show module in plugin lists
flemzord Oct 9, 2026
6389ea6
fix(pluginsdk): reap plugins on remote cancellation
flemzord Oct 9, 2026
5f21e36
refactor(ledger): consume product plugin from misc
flemzord Oct 9, 2026
6829baa
feat(auth): load official Auth plugins through discovery
flemzord Oct 9, 2026
aff263e
refactor(plugins): remove embedded Ledger and Connectivity
flemzord Oct 9, 2026
12e0547
refactor(cmd): separate plugin commands from host orchestration
flemzord Oct 9, 2026
7cd9e0a
refactor(cmd): extract service provider assembly into factory
flemzord Oct 9, 2026
e75ee6b
feat(plugins): resolve checksum-pinned product catalogues
flemzord Oct 9, 2026
7af428c
fix(sdk): decode historical service info envelopes
flemzord Oct 10, 2026
f16f8f2
feat(plugins): cache provider selection per service target
flemzord Oct 10, 2026
d24cbcd
feat(sdk): support command aliases and host file effects
flemzord Oct 10, 2026
69ac729
feat(legacy): restore historical service commands in an SDK plugin
flemzord Oct 10, 2026
b96046b
fix(legacy): wait for terminal workflow instance state
flemzord Oct 10, 2026
fe4a48a
feat(cli): route historical stacks to the legacy service plugin
flemzord Oct 10, 2026
e30a372
build(legacy): package the SDK plugin and document sandbox validation
flemzord Oct 10, 2026
477182a
docs(legacy): record public module and HTTPS input validation
flemzord Oct 10, 2026
e99d741
refactor(legacy): isolate service implementation packages
flemzord Oct 10, 2026
13db825
chore(deps): pin reorganized legacy plugin module
flemzord Oct 10, 2026
6040f16
feat(pluginsdk): add streaming execution and progress events
flemzord Oct 12, 2026
3391261
fix(pluginsdk): preserve stream failures and cancellation
flemzord Oct 12, 2026
a9fdb3c
feat(legacy): stream ledger log imports and exports
flemzord Oct 12, 2026
0f003a9
fix(pluginsdk): normalize streamed input without inline body
flemzord Oct 12, 2026
3945adb
chore(deps): pin streaming plugin SDK
flemzord Oct 12, 2026
9c5b289
feat(cli): render plugin progress and stream legacy files
flemzord Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
102 changes: 102 additions & 0 deletions .agents/skills/formance-fctl-plugin/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
name: formance-fctl-plugin
description: "Create or extend embedded or external Go service plugins in fctl v4: manifests, declarative forms, API operations and contract tests. Use for plugin development rather than operating the CLI."
---

# fctl plugin development

Build service commands that work through both the CLI and a standalone SDK
caller. Keep service behavior in the plugin and terminal/authentication behavior
in the host. Embedded and external plugins use the same public contract. The Ledger pilot
owns its implementation in the Ledger repository's `fctl-plugin` module; fctl
embeds that package or selects an installed executable.

## Establish the contract

Locate the requested fctl checkout and read its `go.mod`, `docs/plugins.md`,
`pkg/pluginsdk/sdk.go` and `pkg/pluginsdk/interaction.go`. These files take
precedence over assumptions about older fctl releases. If the checkout lacks
this SDK, establish the intended version before creating another architecture.

Read the service's published client or API definitions at the requested branch
or version. Use the small Auth plugin for SDK-backed operations, Ledger for
precise JSON and keyset pagination, and Cloud for multi-service commands. Pick
the relevant example rather than copying the whole tree. For Ledger, consult
the current source contract in `docs/validation.md` and the requested release
branch; a deployed beta can differ from that contract.

Map each requested command to its method, versioned route, payload, response,
pagination, and failure behavior. Distinguish missing CLI behavior from an API
capability absent in the deployed service.

## Implement the plugin

For an embedded service, create or extend `plugins/<service>`. For an external
service, create a separate module in the product repository and depend on
`github.com/formancehq/fctl/pkg/pluginsdk`; do not depend on the fctl core module.
Expose `New(*http.Client) pluginsdk.Plugin`,
`GetManifest(context.Context)` and `Execute(context.Context, ExecuteRequest)`.
Metadata must be available with a nil client and without network access.

Normalize every execution with `pluginsdk.NormalizeRequest`, then validate
identifiers and the service payload before issuing a request. Preserve
`ChangedFlags` when omitted values and explicit false/empty values differ.
Return a useful error when execution lacks an injected HTTP client.

Consume the injected client, endpoint and non-secret context. Use the published
service client when it implements the needed contract, or the public
`pkg/pluginsdk/httpclient` boundary for missing operations. Keep authentication
and endpoint selection in the host. Plugin packages depend on public SDK/client
packages, not Cobra, UI libraries, fctl `internal` packages or server commands.

Preserve `json.RawMessage` or `json.Number` for quantities and identifiers.
Pass raw IDs to `httpclient.Path`; it escapes each segment once, including a
slash or literal percent sign. Avoid pre-escaping these inputs. Validate empty
IDs and dot-segment IDs before building service routes. Keep
mutation retries explicit; the shared HTTP boundary performs one attempt.
Return partial response data alongside an error when the API contract requires it.

## Describe the CLI

Declare commands, argument counts, flags and help in the manifest. Use inherited
`Target` for identity/organization/stack selection, and `Service` overrides for
different host connection boundaries. Declare destructive operations with
`Confirm: true` and a boolean `confirm` flag defaulting to false.

For missing-input forms, resource selectors or a payload editor, read
[declarative inputs](references/inputs.md). Put serializable input metadata on
runnable leaves; the core renders the UI and validates the accepted values.

For authorized embedded integration, register the factory in the existing
`cmd/root.go` registry and give its root command the appropriate help group.
For an isolated evaluation, use a temporary host harness and leave the requested
checkout unchanged. A new service may also need host
endpoint, scope and connection configuration support: inspect the current
resolver rather than assuming a new manifest service is automatically usable.
Keep these changes at the connection boundary; routes and payloads remain in
the plugin. Existing service extensions usually need no core changes.

## External executable and distribution

Read `docs/plugin-distribution.md` and the SDK `README.md`. The executable calls
`transport.Serve(factory)`. Supply the exact service version in its manifest;
keep plugin revision separate. Use Ledger's executable and publisher as the
example. Build with the product release, publish public OCI artifacts before
advertising catalogue entries, and generate manifests from the actual binary.
The host owns credentials, HTTP callbacks, checksums, target locks and cached
help. Do not pass tokens in arguments, environment or request context.

Test an actual subprocess through `fctl plugins install` and test a registry
download through `plugins sync`. Cover version drift before mutations, offline
help/completion, both explicit flags and forms, and exact JSON responses.

## Verify and deliver

Read [contract and CLI checks](references/verification.md) for the checks that
apply to the new behavior. A plugin is ready when its API behavior passes local
fixtures, its manifest registers, a standalone caller runs without core/UI
dependencies, and the requested CLI path works in interactive and explicit modes.

Use authorized disposable resources for live verification when requested, then
read back changes and clean up owned fixtures. Report fixture coverage, real
service results, unsupported backend behavior and unverified paths separately.
4 changes: 4 additions & 0 deletions .agents/skills/formance-fctl-plugin/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "fctl Plugin Development"
short_description: "Create decoupled fctl plugins with forms and tests"
default_prompt: "Use $formance-fctl-plugin to add a service plugin to fctl v4 with declarative forms and contract tests."
97 changes: 97 additions & 0 deletions .agents/skills/formance-fctl-plugin/references/inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Declarative forms and selectors

Read the checkout's `docs/interaction.md` and the existing service's
`interaction.go` before choosing fields. `InputSpec` expresses UI intent; it does
not replace argument, flag or payload validation in direct SDK execution.

## Bindings

Each input binds exactly one of `Flag`, `Argument`, `BodyPointer` or `Context`.
`Argument` is a zero-based index represented by `*int`; zero must not disappear
during JSON serialization. `BodyPointer` is an RFC 6901 pointer into the request.
`Context` is reserved for host organization/stack IDs.

Supported kinds are `input`, `text`, `select` and `confirm`; the value type is
`string`, `json`, `number` or `bool`. Choose types from the actual service
contract. A numeric input does not expand the supported `FlagSpec.Type` set.
Declare every bound flag and keep it consistent with the input's value type.
`number` checks JSON numeric syntax, not an integer range or sign. Enforce
integer, nonnegative and size constraints in plugin payload validation according
to the service contract. Validate exact tokens or `json.Number` with integer
arithmetic instead of converting through `float64`; preserve the API's allowed
numeric notation and bounds.

For example, a leaf creating an object can declare:

```go
pluginsdk.CommandSpec{
Use: "create", Short: "Create an item", Runnable: true,
Flags: []pluginsdk.FlagSpec{{
Name: "data", Type: "string", Body: true, Required: true,
Usage: "JSON object, @file or - for stdin",
}},
Inputs: []pluginsdk.InputSpec{
{Title: "Item name", Kind: "input", BodyPointer: "/name",
ValueType: "string", Required: true},
{Title: "Metadata", Kind: "text", BodyPointer: "/metadata",
ValueType: "json", Description: "Optional JSON object"},
},
}
```

The host reads inline/file/stdin body flags; SDK callers provide `Body` directly.
An explicit body bypasses JSON-pointer fields. Use `AlternativeArgument` for a
positional equivalent of a flag/context input. Use `AlternativeFlag` only for a
real declared alternative, such as `data` bypassing a payload-related flag.

Explicit arguments, flags, JSON fields, environment settings and saved targets
take precedence. Without explicit body input, missing required values open the
form. An explicit body bypasses the form even when incomplete: the plugin then
rejects missing required fields. Complete invocations run directly. Optional
blank fields stay omitted. Update forms should preserve
existing options unless the caller explicitly changes or clears them.

## Discover choices

Declare the read operation in the same plugin, then reference it with a full
`CommandPath`. The host injects a GET-only client for choice discovery.
For a runnable `show ITEM_ID` leaf with exactly one argument:

```go
pluginsdk.InputSpec{
Title: "Item", Kind: "select", Argument: new(0),
ValueType: "string", Required: true,
Source: &pluginsdk.ChoiceSource{
CommandPath: []string{"inventory", "list"},
ValueField: "id", LabelFields: []string{"name", "id"},
EmptyMessage: "No items are available; create one first",
},
}
```

`ValueField` is the identifier passed to execution; labels are display only.
Source arguments and flags can refer to previously resolved `$arg0`, `$flag`,
`$organization` or `$stack`. Propagate any required experimental opt-in flag.
Secret fields use `Secret: true`, kind `input`, a string value type and no
default. Choice labels must not expose credentials.

Return the service's supported list envelope, such as `data` or `cursor.data`.
For cursor pagination preserve `hasMore` and `next`. For keyset pagination use
`AfterField` with the exact final-row identifier and declare the list's
`page-size` flag as `uint32` and `after` as `string`. Cursor lists need a
declared string `cursor` flag for subsequent pages. The core enumerates pages and
deduplicates choices; the plugin still implements the actual pagination API.

`ExcludeTrueFields` hides entries with a true boolean field. `MatchFields` ANDs
the declared fields and accepts any listed value per field. These filters can
restrict eligible lifecycle states. Pagination advances from original rows
before filtering, so an ineligible full page must not terminate discovery.
`PreferredPrefix` orders suggestions; it does not authorize selecting a fallback.

## Host behavior to preserve

Forms require terminal stdin and stderr and write to stderr; results write to
stdout. `--no-input`, `FCTL_NO_INPUT` and `CI` disable interaction. The host owns
cancellation, styles, keyboard navigation and confirmation defaulting to No.
Use metadata to extend these behaviors instead of adding prompts or output
calls to service code. `--confirm=false` and cancellation must cause no mutation.
78 changes: 78 additions & 0 deletions .agents/skills/formance-fctl-plugin/references/verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Verify a plugin independently and through fctl

Use local HTTP fixtures for API behavior before relying on a real stack. Read
the checkout's test conventions and `justfile`; use its pinned Go/Nix toolchain.

## API and manifest evidence

- Call `GetManifest` with a nil HTTP client and marshal/unmarshal the manifest.
Register it through the existing registry tests to catch invalid bindings,
command paths, choice sources and inherited flags.
- Execute each added leaf through the public SDK against `httptest.Server`.
Check method, escaped route, query, body and relevant headers. Verify wrong
argument counts, unknown flags, invalid payloads and refused confirmations
fail before HTTP. Include malformed/error responses relevant to the API.
- Exercise omitted update options versus explicit false/empty values when
supported. Verify `9007199254740993` survives a request and a response exactly
when the operation carries quantities, metadata or numeric IDs.
- Check cancellation and write count for mutations. Add idempotency/partial
result checks when those behaviors belong to the service contract.

Use the public API, not implementation-mirroring assertions. A standalone
consumer can use a temporary module with a local `replace` for
the public SDK and product plugin modules (or `github.com/formancehq/fctl/v4`
for an embedded service), then import the plugin and call `New`,
`GetManifest` and `Execute`. Use `GOWORK=off` so workspace settings do not hide
dependencies. Inspect its dependency closure for Cobra, pflag, UI and fctl core
packages; none belongs to the plugin's public execution path. Do not treat the
host CLI's own dependency closure as the plugin closure.

Registry and Runner integration checks belong in the fctl module, because those
host packages are `internal`. For an external plugin module, keep its SDK tests
independent and place host integration tests in fctl or an isolated host checkout.
The standalone consumer must exercise the public dependency boundary separately.

## Forms and scripting

Use the core's injected `interactive.Runner` in adapter fixtures to cover:

- Missing values bind to the expected argument/flag/body/context without an
extra write; cancellation and default-No confirmation perform no mutation.
- Explicit values and a complete `Body` skip forms, including inline JSON,
`@file` and stdin input. Disabled interaction returns an actionable error.
- Selectors return actual IDs, handle empty lists and use GET-only discovery.
If paginated, test a second page and repeated/missing continuations; if filtered,
test a full ineligible page followed by an eligible page.

Use a real PTY when adding a new interactive path. Check one complete form,
resource selection and cancellation at ordinary terminal dimensions. Capture
stdout separately from stderr and parse `-o json` even with forced color. Check
human tables with `NO_COLOR`. Runner fixtures cannot verify visual rendering or
actual keyboard navigation.

## Repository checks and live work

For authorized new plugins or host integration, run the repository checks,
for example:

```bash
nix develop --impure --command just pc
nix develop --impure --command just tests
```

Inspect changes from formatter/generator recipes before committing. An isolated
SDK consumer needs its own `go test -race ./...`; the fctl suite does not discover
tests in an external module automatically. When adding a host service boundary,
cover Cloud, anonymous and client-credentials modes in connection fixtures.

For a read-only or temporary evaluation, run formatting, race tests, vet and
dependency checks only in the temporary module or host copy. `just pc` writes
module, generated, lint-fix and completion files; it belongs in the writable
integration checkout, not a read-only source checkout. Report this smaller scope.

When live testing is in scope, use the selected sandbox and newly owned fixtures.
Read back mutations, retain user-owned resources and verify cleanup. Report the
selected stack version and actual service version separately. Deployed betas
can lack fields present in a release branch; fixture success proves the client
contract, while live readback proves deployed behavior. CLI work does not grant
permission to publish releases, send invitations or modify unrelated resources.
3 changes: 0 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ jobs:

Dirty:
uses: formancehq/ci/.github/workflows/go-dirty.yml@v1
secrets:
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}

Tests:
uses: formancehq/ci/.github/workflows/go-test.yml@v1
Expand All @@ -40,6 +38,5 @@ jobs:
build-target: release-local
secrets:
NUMARY_GITHUB_TOKEN: ${{ secrets.NUMARY_GITHUB_TOKEN }}
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
1 change: 0 additions & 1 deletion .github/workflows/releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@ jobs:
attestations: write
secrets:
NUMARY_GITHUB_TOKEN: ${{ secrets.NUMARY_GITHUB_TOKEN }}
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
Loading
Loading