# Appmixer CLI

The command-line interface to the [Appmixer](https://www.appmixer.com) engine. It covers
the full Appmixer API surface — flows, components, accounts, users, data stores, logs,
system administration — and the complete *connector development lifecycle*: scaffold,
test, pack, publish, and E2E-test custom components against a live instance.

Every command supports `--json` for machine-readable output, which makes the CLI equally
usable by humans and by AI agents (see [appmixer-skills](https://github.com/Appmixer-ai/appmixer-skills),
which drives this CLI as its only external tool).

Visit [https://docs.appmixer.com/appmixer-cli/appmixer-cli](https://docs.appmixer.com/appmixer-cli/appmixer-cli) for more information.

## Installation

```sh
npm install -g appmixer
appmixer --version
```

Requires Node.js >= 18.15.

## AI Development

AI-assisted connector development lives in the public
[appmixer-skills](https://github.com/Appmixer-ai/appmixer-skills) repository:
agent skills that drive this CLI to build, test, and review connectors
end-to-end (`build-connector`, `test-connector`, `review-connector`),
installable as a Claude Code plugin.

## Configuration & Authentication

Point the CLI at your instance and log in:

```sh
appmixer url https://api.your-instance.com   # must be the API host, not the designer UI
appmixer login your@email.com                # prompts for the password, stores a JWT
appmixer login --sso                         # SSO accounts: signs in through the browser
```

Both are stored in `~/.config/configstore/appmixer.json` (see [Alias Storage](#alias-storage)
for multi-instance aliases). The stored session is all any command needs — including the
`e2e` commands. See [Single sign-on](#single-sign-on) for the SSO flow.

Sessions are kept **per instance**, so logging in twice is a one-time cost:

```sh
appmixer login me@example.com prod   # log in and remember prod
appmixer login me@example.com qa     # …and qa
appmixer alias qa                    # switch: URL *and* session
appmixer alias                       # aliases; * marks the ones you are logged into
```

Tokens live in that file (`0600`, no encryption at rest — the same posture as
`~/.aws/credentials` or `~/.kube/config`). `appmixer logout` forgets the current
instance, `appmixer logout --all` forgets every one.

**Optional environment overrides** take precedence over the stored configuration when set —
useful for CI and headless agent workflows, never required:

| Variable | Effect |
|----------|--------|
| `APPMIXER_API_URL` | API base URL; overrides the stored `appmixer url`. |
| `APPMIXER_TOKEN` | Pre-obtained JWT; skips `appmixer login` entirely (CI, and SSO accounts in non-interactive shells — interactively use `appmixer login --sso`). |
| `APPMIXER_SKILL_API_URL` / `APPMIXER_SKILL_USERNAME` / `APPMIXER_SKILL_PASSWORD` | Alternative credentials for the `e2e` commands (e.g. a dedicated e2e user in CI); loaded from exported vars, the file `$APPMIXER_ENV` points to, or `~/.config/appmixer-skills/env` (in that precedence). Without them the `e2e` commands use the stored CLI session; `APPMIXER_TOKEN` wins over everything. |
| `APPMIXER_WORKSPACE` | Workspace every command runs in — name or id; the `--workspace` flag does the same per invocation. |
| `APPMIXER_NO_BANNER` | Silences the one-line workspace notice printed to stderr when a command runs outside the personal context. |
| `APPMIXER_SKILL_ACCOUNT_ID` | Pins the account `appmixer e2e import` binds to connector components (the `--account` flag does the same per invocation). |
| `APPMIXER_SKILL_CONNECTORS_DIR` | Connectors workspace root for the `e2e` commands and validators when running outside the workspace (`--connectors-dir` does the same per invocation). |
| `APPMIXER_SKILL_UI_URL` | Designer base URL used to print clickable flow links in E2E results; without it the flowId is printed. |

## Conventions

- **`--json`** — every command that returns data prints machine-readable JSON to **stdout**
  (the file-producing ones — `pack`, `download`, `file download`, `flow metrics` — write a file instead);
  progress and diagnostics go to **stderr**, so `appmixer flow get X --json > flow.json`
  and piping into `jq` are always safe.
- **Errors** print to stderr; under `--json` they are emitted as a JSON object
  (`{ "error": ..., "status": ..., "method": ..., "url": ... }`).
- **Exit codes** — `0` success, `1` failure. The `e2e` commands add task-specific codes
  (`appmixer e2e run --fix`: `2` = FIX BRIEF emitted; `appmixer e2e results`: `1` = failed
  records exist), documented in each command's `--help`.
- **Payload input** — commands that take a JSON payload accept `--data '<json>'`,
  `--file <path>`, or piped stdin.
- **`--workspace <name|id>`** — runs a single command in a workspace,
  see [Workspaces](#workspaces). `APPMIXER_WORKSPACE` does the same for every command.

## Usage
```
$ appmixer --help
Usage: appmixer [options] [command]

Appmixer command line interface.

Options:
  -v, --version        output the version number
  -h, --help           output usage information

Commands:
  account|acc          Service account commands.
  acl                  ACL commands (admin).
  alias|a              Use alias.
  app                  App commands.
  auth                 Authentication commands (component/account bindings, OAuth helpers).
  automation-hub       Automation Hub commands.
  bundle               Bundle commands.
  category             Category commands.
  chart                Chart commands.
  component|c          Component commands.
  connector            Connector source validation commands.
  config               Instance-level system config commands (admin).
  dead-letter          Dead-letter queue (unprocessed messages) commands.
  download|d           Download component.
  e2e                  E2E test flow commands (import, run, results).
  file                 File commands.
  flow|f               Flow commands.
  init|i               Initialize component.
  ai                   AI tools (deprecated — use appmixer-skills).
  login|l              Login into Appmixer API.
  integration          Integration commands.
  listener             Listener commands.
  logs                 Search or tail instance logs.
  logout|o             Logout from Appmixer API.
  modifiers|m          Modifiers command.
  pack|p               Pack component into archive.
  price-list           Price list commands.
  public-file          Public file commands (admin).
  publish|pu           Publish component.
  remove|rm            Remove component.
  quota                Quota commands.
  resources            Resource commands (admin).
  service-config       Service config commands (admin).
  stats                Statistics commands.
  system               System commands (admin).
  store                Data store commands.
  telemetry            Telemetry commands.
  test|t               Test component, authentication module, ...
  transfer|tr          Transfer resources between instances.
  update|up            Update version of component, module or service.
  url|u <url>          Set Appmixer API url.
  url-alias            URL alias commands.
  user                 User commands.
  user-service-config  Per-user service config commands.
  workspace|ws         Workspace (user group) commands.
  help [cmd]           display help for [cmd]

Go to https://docs.appmixer.com/appmixer/ to find more information.
```

## Command Reference

Run `appmixer <command> --help` for the full list of options of any command.

### url

Sets the Appmixer API URL that all other commands (`login`, `publish`, `test`, ...) will be executed against. Optionally stores the URL under an alias so you can switch between instances easily.

```sh
appmixer url https://api.appmixer.com
appmixer url http://localhost:2200 local

# List stored URLs (* marks the ones you are logged into):
appmixer url -l
```

Sessions are per instance, so this brings the target instance's session along; if there is
none it says so rather than leaving the previous instance's token active.

### alias

Switches to an instance previously stored with `appmixer url <url> <alias>` — both the URL
**and** that instance's session, so you only log in to each instance once.

```sh
appmixer alias prod
# Using prod
# URL: https://api.appmixer.com
# Logged in as me@example.com.

# Without arguments, prints all stored aliases; * marks the ones with a session:
appmixer alias
```

If you have never logged into that instance (or ran `appmixer logout` there), it warns and
leaves you unauthenticated instead of carrying a foreign token over.

### login / logout

Logs you in against the currently set API URL (or a URL alias) and stores the session used by the other commands; `logout` forgets it again, for that instance. Accounts without a password (SSO) use `--sso`, which signs in through the browser — see [Single sign-on](#single-sign-on).

```sh
appmixer login your@email.com

# Login against a URL alias:
appmixer login your@email.com production

# Print the stored JWT token:
appmixer login -t

# SSO account (no password) — signs in through the browser:
appmixer login --sso

# Log straight into a workspace:
appmixer login your@email.com --workspace "QA team"

appmixer logout          # forget the session for the current instance
appmixer logout --all    # forget every stored session
```

Each login is remembered against the instance it was made on, so `appmixer alias qa`
switches the URL *and* restores that instance's session instead of leaving the previous
instance's token to 401 against the new host. `appmixer alias` with no argument lists the
aliases and marks the ones with a stored session.

#### Single sign-on

SSO accounts have no password to send to `/user/auth`, so `appmixer login --sso` gets the
JWT out of a browser round-trip instead. It stores exactly what `appmixer login` stores —
every other command is unaware of how the session was obtained.

```sh
appmixer login --sso                    # opens the browser, waits for the callback
appmixer login --sso --manual           # no local server: paste the URL you land on
appmixer login --sso --port 8080        # callback port registered with the IdP
appmixer login --sso --no-browser       # print the login URL instead of opening a browser
appmixer login --sso --timeout 600      # seconds to wait for the callback (default 300)
```

Flags: `--manual`, `--port <port>` (default `9876`), `--timeout <seconds>` (default `300`),
`--no-browser`. There is no email argument — the identity provider identifies you, and
gridd's SSO endpoints take no email hint.

What happens:

1. `POST /users/sso/redirect { redirectUri }` returns the IdP login URL, where `redirectUri`
   is the CLI's own `http://localhost:<port>/sso/callback`. (The plural `/users/` here versus
   the singular `/user/` below is not a typo — gridd registers the routes that way.)
2. You sign in. **OIDC** sends the browser back with `?code=...`; **SAML** posts the assertion
   to gridd (`POST /user/sso/saml`), which redirects the browser to the same callback with the
   auth-state id as the code.
3. `POST /user/sso/auth { redirectUri, code }` exchanges it for the session.

For **SAML** the callback URL carries a `state` nonce (`?state=<uuid>`) that is verified on
return — gridd relays through its own `RelayState`, so nothing has to be registered anywhere.
For **OIDC** the redirect URI is sent bare, because IdPs match the registered `redirect_uri`
by exact string (several reject registered URIs with a query at all): the admin has to
register `http://localhost:9876/sso/callback` — or whatever `--port` you standardise on.

`--manual` starts no local server. Sign in, let the browser fail to reach localhost, and
paste the URL from the address bar — the code is in it. A bare code works too, as does a JWT
copied out of an already signed-in designer session. Use it on remote shells, in containers,
and wherever the IdP will not redirect to localhost.

The token is verified against `GET /user` before the stored session is overwritten, so a
failed SSO login never leaves you logged out of a working session. `--sso` is interactive
by design; for CI use `APPMIXER_TOKEN` with a pre-obtained JWT.

`POST /user/sso/refresh` (it takes the `refreshToken` that `/user/sso/auth` returns) has no
dedicated CLI command and the CLI does not store the refresh token — re-run
`appmixer login --sso` when the session expires.

### init

Scaffolds new files so you don't have to start from scratch:

- `appmixer init component` – initialize a new component.
- `appmixer init auth` – initialize an `auth.js` authentication file.
- `appmixer init example` – initialize an example component with service, module and authentication.
- `appmixer init openapi` – initialize a service from an OpenAPI specification.
- `appmixer init template` – initialize a service from an Appmixer Service Template.
- `appmixer init mcp <repository> [outputDirectory]` – generate a connector from an MCP server package
  (`--label`, `--desc`, `--icon`, `--docs`, `--env`, `--repositoryVersion`, `--scriptPath`, `--replace`).

### pack

Creates a zip archive from a directory containing `component.json`, `service.json` or `module.json` (a single component, a whole service or a module respectively). If no path is given, the current working directory is used.

By default, `node_modules`, `.git` and all hidden files and directories (starting with a dot) are excluded from the archive. Use `--include-hidden` to include hidden files and directories (`.git` is always excluded).

```sh
# Pack the current directory:
appmixer pack

# Pack a service, custom output file name:
appmixer pack ./my-vendor/my-service -o my-service.zip

# Include hidden files and directories:
appmixer pack ./my-vendor/my-service --include-hidden
```

### publish

Uploads a packed archive to the Appmixer instance. Without arguments it looks for a zip archive in the current working directory.

```sh
appmixer publish my-component.zip

# Remove components/services that exist on the server but are not in the archive:
appmixer publish --replace-all my-component.zip
```

### download

Downloads component files from the Appmixer instance. The selector can point to a whole service, a module, a single component or a single file.

```sh
# All files of one component:
appmixer download vendor.google.gmail.SendEmail

# A single file of a component:
appmixer download vendor.google.gmail.SendEmail/SendEmail.js

# All gmail components:
appmixer download vendor.google.gmail

# Service-level file:
appmixer download vendor.google.gmail/auth.js
```

### remove

Removes a component, module or a whole service from the Appmixer instance.

```sh
appmixer remove your-vendor.google.gmail.SendEmail   # one component
appmixer rm your-vendor.google.gmail                 # all module components
appmixer rm your-vendor.google                       # the whole service
```

### update

Bumps the version (`major`, `minor` or `patch`) in `component.json`, `module.json` or `service.json` and automatically updates the dependencies that reference it. When you increase a version of a service, the dependency is updated in all `module.json` and `component.json` files in the subdirectories; when you increase a version of a module, it is updated in all its `component.json` files — so you don't have to edit them manually.

```sh
# Bump the patch version in the current directory:
appmixer update patch

# Bump the minor version of a module (updates dependencies in its components):
appmixer update minor ./vendor/service/module

# Bump the major version of the whole service:
appmixer update major ./vendor/service
```

### component

Dev tools for components on the instance. All subcommands support `--json`.
Selectors are dotted component paths — `vendor`, `vendor.service`,
`vendor.service.module` or `vendor.service.module.Component`:

```sh
appmixer component ls                           # all component types on the instance
appmixer component ls appmixer.google.gmail     # narrowed by selector
appmixer component ls -m --json                 # full manifests instead of type names

appmixer component get appmixer.google.gmail.SendEmail -o SendEmail.zip   # sources as zip
appmixer component publish my-service.zip       # same as `appmixer publish`
appmixer component rm your-vendor.google.gmail  # same as `appmixer remove`
appmixer component status <ticket> --json       # poll an upload (publish returns the ticket)
```

`component call` executes a component function **directly**, without building a
flow — the fastest way to test a connector operation against the real service.
The payload must reference a `componentId` (or `--caller`) whose account is used
for authentication:

```sh
appmixer component call appmixer.google.gmail.ListLabels \
    -c <componentId> -d '{"in":{}}' --json
appmixer component call <type> -c <componentId> -f input.json --out-port out
```

### connector

Local (offline) validation of connector SOURCE code — the bundle.json /
component.json standards suite ported from the connectors repository. It runs
over a connectors workspace (`src/<vendor>/<connector>/...`, any vendor) found
via `--connectors-dir`, `$APPMIXER_SKILL_CONNECTORS_DIR`, or by walking up from
the cwd. Two mechanisms keep legacy debt manageable: a **thresholds file**
(ratchet) caps the allowed failure count per validator — a run fails only on a
regression, and `--update-thresholds` writes lowered counts back as debt is
paid down — and an **ignore-list** suppresses specific known-false-positive
reports, each with a recorded reason (`--show-ignored` lists them). Validators
without a threshold entry are strict; `--changed` and a connector-scoped run
are always strict.

```sh
appmixer connector validate                     # full workspace run (thresholds apply)
appmixer connector validate todoist             # one connector, strict, warnings printed
appmixer connector validate acme/crm --json     # report object on stdout
appmixer connector validate --changed --base origin/dev   # strict on files changed vs the base ref
appmixer connector validate --update-thresholds # tighten the ratchet when counts dropped
appmixer connector validate --show-suppressed   # failures held under thresholds + warnings
```

Flags: `--connectors-dir <dir>`, `--changed`, `--base <ref>` (default `dev`),
`--rules-dir <dir>` (extra rules, additive), `--ignore-file <path>`,
`--thresholds-file <path>`, `--update-thresholds`,
`--show-suppressed [validator]`, `--show-ignored [validator]`,
`--show-warnings`, `--json`. Default workspace files:
`validators.ignore.js|.json` / `validators.thresholds.json` in the workspace
root, falling back to the connectors repo's
`scripts/validators/_ignore-list.js` / `.thresholds.json`. Exit code 0 = clean
or within thresholds, 1 = regressions or strict failures.

Interop with the connectors repository: pass `--rules-dir scripts/validators`
there to also run its repo-local git-workflow rules (`bundle-bump-on-change`,
`oauth-scope-bump`) — extra rules use the exact same validator contract, and a
local rule whose name matches a built-in is skipped — the built-in wins, so stale local copies cannot shadow upstream fixes.

Maintainer guide — rule contract, adding a rule, thresholds/ignore semantics:
[`src/connector-validators/README.md`](src/connector-validators/README.md).

**`connector verify`** is the live counterpart: where `validate` checks the
SHAPE of the source offline, `verify` executes the connector's behavior files
against the real service API and checks what no static rule can see —
declared output schemas vs the live payload, leaf by nested leaf (a
declared-but-never-returned field is a dead entry in the designer's variable
picker; with `required` in the schema an absent optional leaf only warns),
triggers sampled through their `test()` method, and `--write`
enum round-trips that create a record per select option and compare the
service's own label for the stored value against the inspector label
(catches inverted label/value maps). Fixture recipes live in the connector's
`artifacts/verify.json`; `--record` saves PII-sanitized output shapes to
`artifacts/samples/` and `--offline` re-checks conformance against them with
no credentials at all (the CI-friendly leg).

```sh
appmixer connector verify cliniko                    # schema conformance, live
appmixer connector verify cliniko --write            # + enum round-trips (creates records)
appmixer connector verify cliniko --record           # save sanitized output shapes
appmixer connector verify cliniko --offline          # conformance from samples, no credentials
appmixer connector verify acme/crm --auth auth.json  # explicit credentials (CI)
```

Flags: `--connectors-dir <dir>`, `--auth <file>`, `--write`, `--record`,
`--offline`, `--json`. Credentials default to the `appmixer test auth login`
store. Guide: [`src/connector-verify/README.md`](src/connector-verify/README.md).

### app / bundle

Read-only catalogs of what is installed on the instance:

```sh
appmixer app ls --json          # apps (services with designer metadata: label, icon, categories)
appmixer app components --json  # components grouped per app
appmixer bundle ls --json       # installed connector bundles and their versions —
                                # e.g. check a connector's version before running its E2E flows
```

### flow

Complete flow management on the Appmixer instance — CRUD, lifecycle, versions,
drafts, triggers, test runs and variables. All subcommands support `--json` for
machine-readable output (agents and scripts are first-class consumers); without
it a human-readable summary is printed. Errors exit with code 1 and, with
`--json`, emit a structured `{ "error", "status", "method", "url" }` object on
stderr.

#### CRUD & inspection

```sh
appmixer flow ls                        # list flows (-t <type> to filter, -d for descriptors)
appmixer flow get <flowId>              # flow detail; -p limits/excludes fields, e.g.:
appmixer flow get <flowId> -p name,stage --json
appmixer flow get <flowId> -p -flow,-thumbnail --json   # everything except the descriptor & thumbnail

appmixer flow create flow.json          # create from a JSON file ({ "name": ..., "flow": ... })
appmixer flow update <flowId> flow.json # replace the definition
appmixer flow update <flowId> flow.json --force-update  # a running flow can only be updated with this
appmixer flow update <flowId> flow.json --no-validate --in-progress-data keep

appmixer flow export <flowId> -o my-flow.json   # save name/flow/svg/customFields/... to disk
appmixer flow import my-flow.json               # create a new flow from an exported file

appmixer flow clone <flowId> -n "My copy"       # clone; -t sets the clone's type
appmixer flow remove <flowId>
appmixer flow count -f stage:running --json     # {"count": 12}; -f is repeatable, -p matches name/flowId
appmixer flow components <flowId> --json        # manifests of the components the flow uses
appmixer flow thumbnail <flowId> screenshot.png # set the flow thumbnail (stored base64)
```

#### Lifecycle & diagnostics

```sh
appmixer flow start <flowId>
appmixer flow stop <flowId>
appmixer flow status <flowId> --json    # coordinator status of a (starting/stopping) flow
appmixer flow validate <flowId> --json  # server-side validation: missing required inputs,
                                        # components without a connected account, errorHandling shape
appmixer flow validate-variables <flowId>  # check transform variables against designer offerings
                                        # ("red chip" detection); exit 1 on any INVALID variable
appmixer flow metrics <flowId> -o metrics.html  # self-contained report page; it embeds your session
                                                # token so the browser can call the API — do not share it
```

### E2E test flows

E2E test flows live in the connectors repo under
`src/<vendor>/<connector>/artifacts/test-flows/test-flow-*.json` and are identified
on the instance by customFields: `category=E2E_test_flow`,
`connector=<vendor>:<connector>` (e.g. `appmixer:google:gdrive`) and `name=<test case>`.

```sh
appmixer e2e import <file|dir>          # create/update by identity: stamps customFields, ensures the
                                        # E2E result stores, binds accounts (+ validity preflight),
                                        # validates variables server-side; local validation on by default
appmixer e2e list [-c <ref>]            # list E2E flows (flowId, stage, connector, name)
appmixer e2e run <flowId>               # start + watch THE CURRENT RUN's logs only; exit 0 pass / 1 fail
appmixer e2e run <flowId> --fix         # + deterministic triage loop (rebind on TokenError, retry on
                                        # transient infra); exit 2 = FIX BRIEF for the calling agent
appmixer e2e results [--clean]          # read the success/fail result stores (exit 1 on failed records)
appmixer e2e export [flowId|-c <ref>]   # pull flows back into the connectors repo, stripped of
                                        # instance state (ids, store bindings, identity customFields)
appmixer e2e validate <file|dir>        # local validator suite (29 rules, --ruleset e2e|basic)
appmixer e2e rm <flowId>|-c <ref>       # delete E2E flows (refuses non-E2E flows)
```

A typical agent loop:

```sh
appmixer e2e import ./src/appmixer/todoist/artifacts/test-flows --json   # exit 1 = fix flows first
flowId=$(appmixer e2e list -c appmixer:todoist --json | jq -r '.[0].flowId')
appmixer e2e run $flowId --fix --json   # exit 0 pass | 2 = FIX BRIEF: edit JSON, re-import, re-run
appmixer e2e results -c appmixer:todoist --json
```

#### Test mode (no running flow needed)

Executes a single component and traverses the flow downstream, streaming
per-component results in real time — the API behind the Designer's *Test flow*
button. The flow's stage is not changed.

```sh
appmixer flow test <flowId> -c <componentId>                 # stream test results to stdout
appmixer flow test <flowId> -c <componentId> -d '{"q":"x"}'  # with trigger payload
appmixer flow test <flowId> --abort <testRunId>              # abort a running test
```

#### Triggers & webhooks

```sh
appmixer flow trigger-url <componentId>              # print the public webhook URL of a trigger
appmixer flow trigger <flowId> <triggerId> -d '{"hello":"world"}'   # POST to it via the API
appmixer flow trigger <flowId> <triggerId> -X GET    # triggers accept GET/PUT/DELETE too
appmixer flow send <flowId> message.json             # dispatch a message to a running flow (admin)
```

#### Versions

Named snapshots of a flow (`manual`, plus automatic `autosave`/`publish`/
`restore-point` types). Restoring a *running* flow never modifies it directly —
a draft with the version's content is created instead; a stopped flow is
restored in place.

```sh
appmixer flow version create <flowId> -l "before refactoring"   # snapshot the current state
appmixer flow version ls <flowId> --json
appmixer flow version get <flowId> <versionId> --json           # includes the flow descriptor
appmixer flow version label <flowId> <versionId> "v1.2 release"
appmixer flow version restore <flowId> <versionId>
appmixer flow version clone <flowId> <versionId>                # new flow from the snapshot
appmixer flow version rm <flowId> <versionId>
appmixer flow version gc <flowId>                               # clean up old auto versions (admin)
```

#### Drafts

Drafts are editable copies of a flow (created in the Designer or via
`flow clone` with draft parameters) that let you change an integration while
the original keeps running. Publishing merges the draft back into its origin
flow:

```sh
appmixer flow draft publish <draftFlowId>
```

#### Variables

Variables are the placeholders usable in component configs/inputs (outputs of
upstream components, flow variables). Useful when generating or repairing flow
JSONs programmatically:

```sh
appmixer flow variables <flowId> --json                    # all flow variables
appmixer flow variables <flowId> -c <componentId> --examples   # variables visible to one component
```

### account

Service accounts are the stored credentials (OAuth tokens, API keys) that
components use to talk to third-party services. Normally an account is created
when a user authenticates a component in the Designer; the CLI lets you manage
them headlessly — including *injecting* accounts without any user interaction,
which is how Integrations and E2E test setups provision credentials. All
subcommands support `--json`.

```sh
appmixer account ls                     # list accounts of the authenticated user
appmixer account ls -s appmixer:slack   # only accounts of one service
appmixer account get <accountId> --json
appmixer account update <accountId> "Work Slack"    # rename (displayName)
appmixer account test <accountId>       # run the connector's auth validate() server-side —
                                        # catches expired/revoked tokens before a flow run
appmixer account rm <accountId>         # revoke (delete) the account
appmixer account rm-all <userId>        # delete all accounts of a user (admin)
```

Inject an account directly (no OAuth popup). The `service` must have an
authentication module (`auth.js`) installed; the `token` shape depends on the
auth type (OAuth1, OAuth2, API key):

```sh
cat > slack-account.json <<'JSON'
{
    "service": "appmixer:slack",
    "displayName": "CI bot account",
    "token": { "accessToken": "xoxb-...", "scope": ["channels:write", "chat:write"] },
    "profileInfo": { "name": "ci-bot" }
}
JSON
appmixer account create slack-account.json --json   # → { "accountId": "..." }

# For test/CI accounts with fake credentials, skip the server-side checks:
# --no-validate-scope    don't validate the token scope against installed components
# --no-profile-info      don't call the auth module's requestProfileInfo (provide profileInfo in the file)
appmixer account create test-account.json --no-validate-scope --no-profile-info --json
```

Relations to flows:

```sh
appmixer account flows <accountId>      # which flows use this account
appmixer account for-flow <flowId>      # component → account assignment map of a flow
appmixer account share <accountId> <flowId> <componentId1> <componentId2>
appmixer account unshare <accountId> <flowId>
appmixer account profile-info email -c <componentId>   # profile field via a component's account
```

### auth

Component↔account bindings and the OAuth authorization flow, headless. The key
operation is `bind`: after **any** flow definition update, newly written
connector components are *unbound* — they hold no account — so agents and
scripts must re-bind before starting the flow (the same "Connect account" step
the Designer does). All subcommands support `--json`.

```sh
appmixer auth bind <componentId> <accountId>        # bind one component (designer "Connect account")
appmixer auth bind-account <accountId> <c1> <c2>    # bind several components at once
appmixer auth unbind <componentId>
appmixer auth accounts-map <flowId> --json          # auth state of every component in a flow:
                                                    # { components, errors, services }
```

Typical repair loop after a flow update:

```sh
accountId=$(appmixer account ls -s appmixer:slack --json | jq -r '.[0].accountId')
appmixer auth bind <componentId> $accountId
appmixer flow start <flowId>
```

Service/auth introspection and the OAuth popup flow (what the Designer does
under the hood — usable for custom auth tooling):

```sh
appmixer auth status appmixer:google:gmail --json   # authentication status of a service module
appmixer auth type appmixer:google:gmail            # auth type descriptor (oauth2, apiKey, ...)
appmixer auth ticket --json                         # 1. create a session ticket
appmixer auth url appmixer:google:gmail <ticket>    # 2. URL to authorize the service in a browser
appmixer auth ticket-status <ticket> --json         # 3. poll until the user completed the consent
```

Switching workspace is a `workspace` concern, not an `auth` one — see
[Workspaces](#workspaces).

### user

User management. `appmixer user me` works for everyone; listing, inspecting and
modifying *other* users requires the `admin` scope. All subcommands support
`--json`.

```sh
appmixer user me --json                 # profile of the authenticated user
appmixer user create john@example.com secretPassword   # sign up a user (-u sets a username)
appmixer user ls -p john@ --json        # list users, -p filters by pattern (admin)
appmixer user get <userId> --json
appmixer user count --json              # {"count": 42}
appmixer user update <userId> -d '{"scope":["user","admin"]}'   # e.g. grant admin scope
```

Deleting a user removes all their resources and runs as a background task —
poll the returned ticket:

```sh
ticket=$(appmixer user rm <userId> --json | jq -r .ticket)
appmixer user delete-status <userId> $ticket --json
```

Passwords:

```sh
appmixer user change-password --old <current> --new <new>   # own password
appmixer user reset-password john@example.com <newPassword> # any user (admin);
                                                            # rejected for SSO-managed users
```

Workspaces are shared: every resource in one (flows, accounts, ...) belongs to all its
members, and admins are automatically members of the "admin" workspace. Creating them,
managing membership and switching between them all live under
[`appmixer workspace`](#workspaces).

SSO sign-in is a `login` concern, not a user-management one — see
[Single sign-on](#single-sign-on).

#### Workspaces

The designer calls them Workspaces; gridd calls the same thing a user group, and `src/api`
keeps gridd's names — the CLI says **workspace** everywhere you type.

The workspace context lives **only in the JWT**: every workspace has a shadow user that
owns its resources, and ownership follows whichever user the token identifies. No resource
endpoint takes a workspace parameter — working in one means holding a token minted for it
(`POST /auth/switch-context`).

```sh
appmixer workspace ls                  # yours; * marks the active one
appmixer workspace ls --all            # every workspace on the instance (admin)
appmixer workspace get <workspaceId>   # one workspace (admin)
appmixer workspace use "QA team"       # by name…
appmixer workspace use 66f1c0de…       # …or by id
appmixer workspace use --personal      # back to My workspace
appmixer workspace current             # where am I, and against which instance
```

`appmixer workspace ls` is where the ids come from — it prints `name  id`. Anything that is
not a 24-character ObjectId is treated as a name and looked up there, so the id is rarely
worth typing.

`workspace use` stores the new token, so every later command runs in that workspace until
you switch back. It never prints the token — `appmixer login -t` does.

For one-off work there is no need to switch state at all:

```sh
appmixer flow import flow.json --workspace "QA team"
appmixer flow ls --workspace 66f1c0de0000000000000001
APPMIXER_WORKSPACE=<name|id> appmixer flow ls    # same, for every command (CI, agents)
```

`--workspace` exchanges the token for that invocation only and leaves the stored session
alone. You can also land in a workspace straight from the login:

```sh
appmixer login your@email.com --workspace "QA team"
appmixer login --sso --workspace "QA team"
```

Membership is checked server-side — switching into a workspace you are not a member of is a
403. Administration (create, rename, members) is admin-only:

```sh
id=$(appmixer workspace create "QA team" --json | jq -r .groupId)
appmixer workspace update $id -n "QA & Support"
appmixer workspace member add $id <userId1> <userId2>
appmixer workspace member ls $id --json
appmixer workspace member rm $id <userId1>
appmixer user workspaces <userId> --json    # workspaces a user is a member of
appmixer workspace rm $id
```

**Which commands care.** Ownership follows the token's user, so everything user-owned does:
flows (import, create, clone, versions, drafts, triggers, variables, metrics), service
accounts, data stores, files, integrations, url aliases, listeners, logs, telemetry, quotas
and the `e2e` commands. Instance-level things do not: components and `publish`/`download`,
apps and bundles, instance config, users, groups and ACLs.

The pairing that bites is flows and accounts: a flow imported into a workspace binds to
accounts *in that workspace*, so a flow imported there while its accounts sit in your
personal context comes up unbound.

**Scope travels with the context too.** The token is minted for the group's shadow user and
carries that user's scope — switching into a workspace can hand you different permissions
than your own.

Because of that, any command running outside the personal context prints one line to stderr
before it does anything:

```
→ workspace QA team · https://api.your-instance.com
```

stdout is untouched, so `--json` pipelines are unaffected. `APPMIXER_NO_BANNER=1` silences it.

### acl / resources

Access control lists define which roles may perform which actions — there are
two ACL types, `components` and `routes`. Admin only. Rules are arrays of
`{ role, resource, action[], attributes[] }`:

```sh
appmixer acl types --json               # ["routes", "components"]
appmixer acl get components --json      # current rules of a type

cat > acl.json <<'JSON'
[
    { "role": "user", "resource": "appmixer.utils.*", "action": ["read", "use"], "attributes": ["*"] }
]
JSON
appmixer acl set components acl.json

# Introspection — what can be used in the rules of a type:
appmixer acl resources components
appmixer acl actions components
appmixer acl attributes components appmixer.utils.email.SendEmail
```

Resource transfer moves *everything* (flows, accounts, stores, files) from one
user or group to another — e.g. when off-boarding a user. Admin only; the
source is blocked for the duration and the transfer runs as a background task:

```sh
ticket=$(appmixer resources transfer --source-user <userId> --target-user <userId> --json | jq -r .ticket)
appmixer system task-status $ticket     # (system commands; see the system section)
appmixer resources transfer --source-group <groupId> --target-user <userId>   # groups work too
```

### config / service-config / user-service-config

Instance and service configuration. All subcommands support `--json`; every
`set` accepts an inline value, `--file <json>`, or piped stdin.

System config — instance-level key/value settings (admin):

```sh
appmixer config ls --json               # values stored in the DB
appmixer config ls --include-defaults   # including non-overridden defaults
appmixer config set API_NAME "My Appmixer"
echo '{"nested":"value"}' | appmixer config set SOME_KEY   # JSON values via stdin
appmixer config rm API_NAME
```

Service config — per-service settings, most commonly the OAuth application
credentials of a connector. This is the standard instance-setup step after
publishing a connector (admin):

```sh
appmixer service-config set appmixer:google:gmail \
    -d '{"clientId":"...","clientSecret":"..."}'
cat gmail-credentials.json | appmixer service-config set appmixer:google:gmail

appmixer service-config ls --json
appmixer service-config get appmixer:google:gmail --json
appmixer service-config set <serviceId> -u -d '{...}'   # -u = update (PUT) instead of create
appmixer service-config status <serviceId> --json       # is the service configured?
appmixer service-config validate <serviceId> --json     # validate the stored credentials
appmixer service-config rm <serviceId>
```

User service config — per-user *named* variants of the same thing, letting one
user keep several configurations of a service (e.g. two OAuth apps):

```sh
appmixer user-service-config set appmixer:google:gmail work -d '{"clientId":"..."}'
appmixer user-service-config ls --json
appmixer user-service-config get appmixer:google:gmail --json
appmixer user-service-config rm appmixer:google:gmail work   # one named config
appmixer user-service-config rm appmixer:google:gmail        # all configs of the service
```

### store / file / public-file

Data stores are the instance's key/value storage that flows read and write
(the `appmixer.utils.storage` components). The CLI gives you the same access
from scripts — including the key-level record CRUD that E2E result checking is
built on. All subcommands support `--json`.

```sh
storeId=$(appmixer store create "my-results" --json | jq -r .storeId)
appmixer store ls --json
appmixer store get $storeId --json
appmixer store update $storeId "renamed"
appmixer store rm $storeId
```

Records — whole-store views and key-level CRUD (`set` takes an inline JSON or
plain-string value, `--file`, or stdin; `--create` POSTs instead of replacing):

```sh
appmixer store key set $storeId greeting '"hello"' --create
appmixer store key set $storeId config '{"a":1}' --create
appmixer store key patch $storeId greeting "hello again"        # replace the value (string or number
                                                                # only — gridd rejects objects; use `set`)
appmixer store key patch $storeId greeting --rename welcome     # rename the key
appmixer store key get $storeId config --json           # { "key": "config", "value": ... }
appmixer store key rm $storeId greeting

appmixer store records $storeId -l 30 -s updatedAt:-1 --json   # paged record listing
appmixer store count $storeId --json
appmixer store find $storeId "error" --json             # search records by pattern
appmixer store download $storeId -o dump.json           # dump the whole store
```

Files — binary storage used by flow components (attachments, exports). Upload
streams from a local path, download resolves the stored filename automatically:

```sh
fileId=$(appmixer file upload ./data.csv --json | jq -r .fileId)
appmixer file metadata $fileId --json   # { filename, length, contentType, ... }
appmixer file download $fileId          # saves under the stored filename (-o overrides)
appmixer file ls -p csv --json
appmixer file count --json
appmixer file rm $fileId

appmixer file rm-bulk -p tmp-           # bulk delete by pattern; background task
appmixer file status <ticket> --json    # poll the bulk-delete ticket
```

Public files — assets served without authentication (logos, custom CSS for the
Designer). Admin only:

```sh
appmixer public-file upload ./logo.png
appmixer public-file ls --json
appmixer public-file rm logo.png
```

### logs / telemetry / stats / chart / dead-letter

Observability commands. All support `--json`.

`appmixer logs` searches the instance's log index (Elasticsearch). By default it
prints human-readable `timestamp SEVERITY [componentId] message` lines; `--json`
emits the raw hits — this is what agents use for run-completion detection and
error triage:

```sh
appmixer logs --flow-id <flowId>                    # logs of one flow
appmixer logs --flow-id <flowId> -q "severity:error" --json   # -q = ES query-string syntax
appmixer logs --flow-id <flowId> --test-run-id <id> # only records of one test run
appmixer logs --flow-id <flowId> -s 200 --sort gridTimestamp:desc
appmixer logs --flow-id <flowId> --follow           # tail: keeps polling, prints only new
                                                    # records (NDJSON with --json)
```

Telemetry and statistics:

```sh
appmixer telemetry get --json                   # the token's user's telemetry: message counts, running
                                                # flows, active connectors, used apps (from = to = today)
appmixer telemetry get --from 2026-01-01 --to 2026-01-31 --json   # explicit period; gridd needs both
                                                # bounds, so an omitted one is filled in (there is no all-time view)
appmixer telemetry flow <flowId> --json # per-flow message count and total message size (no error
                                        # counts — for those use `appmixer logs -q "severity:error"`)
appmixer telemetry messages --json
appmixer stats component-usage --json   # which components are used in how many flows
appmixer stats component-usage --build  # (re)build the usage statistics first
```

Charts (saved Insights dashboards):

```sh
appmixer chart create -d '{"name":"Errors per day", ...}'
appmixer chart ls --json
appmixer chart get|update|rm <chartId>
```

Dead-letter queue — messages that failed processing and wait for manual
intervention. Inspect, retry after fixing the cause, or drop:

```sh
appmixer dead-letter ls --json
appmixer dead-letter get <messageId> --json
appmixer dead-letter retry <messageId>
appmixer dead-letter rm <messageId>
```

### system / quota / category / price-list / listener

Instance administration. All subcommands support `--json`; mostly admin-only.

`appmixer system health` is the instance smoke check (replaces hand-rolled
curl in setup scripts):

```sh
appmixer system health --json           # is the instance up and healthy?
appmixer system stats --json            # flows/users/messages counters
appmixer system audits --json           # audit log records
appmixer system drain-status --json     # is the instance draining before shutdown?
appmixer system throttle --json
appmixer system component-jobs --json
appmixer system heapdump -o dump.heapsnapshot   # engine heap snapshot for debugging
appmixer system docs-link appmixer.slack        # docs URL of a module
appmixer system encrypt-tokens          # re-encrypt stored tokens after key rotation
```

Long-running maintenance operations return a ticket to poll:

```sh
ticket=$(appmixer system stop-flows appmixer.slack --json | jq -r .ticket)
appmixer system stop-flows-status $ticket --json    # progress of the stop task
appmixer system stop-flows-cancel $ticket --json    # abandon it
appmixer system task-status <ticket> --json         # generic background-task status
appmixer system task-cancel <ticket>

ticket=$(appmixer system report storage-usage --json | jq -r .ticket)
appmixer system report storage-usage -t $ticket --json   # fetch the finished report
appmixer system report flow-limits                       # same cycle for limits violations
```

Quotas — instance limits and the authenticated user's usage:

```sh
appmixer quota get --json               # quota configuration
appmixer quota set messages -d '{"limit":10000}'    # per-manager quota (admin)
appmixer quota rm messages
appmixer quota test -f quota-rule.json  # dry-run a quota definition (admin)
appmixer quota storage --json           # own storage usage vs. limit
appmixer quota flows --json             # own flow count vs. limit
```

Categories are global labels for integration templates shown in the Automation
Hub (read for everyone, write admin-only); price lists and listeners round out
the admin surface:

```sh
appmixer category create -d '{"name":"Sales"}'
appmixer category ls --json
appmixer category get|update|rm <categoryId>

appmixer price-list ls --json

# A listener is the pair (url, eventName) — gridd identifies it by both.
appmixer listener create appmixer:slack --url https://example.com/hook --event-name message
appmixer listener rm appmixer:slack --url https://example.com/hook --event-name message
appmixer listener rm-all --hostname worker-3.internal   # every listener of a hostname
```

`listener create` also takes `--params '<json>'` for extra listener parameters.

**What a listener is.** A service *plugin* is a single shared receiver: one Slack app has one
Events API webhook, and Slack delivers each event to it once. The listener table is the
subscription registry that plugin fans out from — "when `message` fires on `appmixer.slack`,
deliver it to these URLs" — and the plugin normally drives it from its own context
(`context.triggerListeners`, `getListeners`, `onListenerAdded`). Every record stores the
hostname of its URL, which is what makes `rm-all --hostname` the cleanup for a decommissioned
tenant or node.

These commands are for the instance administrator, not for people building connectors or
flows: another node subscribing remotely (the Auth Hub does this), and cleaning up after a
tenant or node that is gone.

Two shapes follow from that. There is no "remove every listener of this service" form — a
listener is the `(url, eventName)` pair and that is what gets removed, or a whole hostname.
And the API is **write-only**: gridd exposes no `GET /listeners`, so registrations cannot be
listed or inspected, only added and removed.

### integration / url-alias / automation-hub

Integrations are template-based flows: a *draft* is edited, published into an
*integration template*, and users create *instances* of it. When the template
changes, all instances have to be rolled forward — that is what
`update-instances` does (a background operation returning a ticket). All
subcommands support `--json`.

```sh
ticket=$(appmixer integration update-instances <integrationTemplateId> --json | jq -r .ticket)
appmixer integration update-instances <id> --in-progress-data delete   # drop in-flight data
appmixer integration status $ticket --json          # poll until state=completed
appmixer integration rm <flowId>                    # delete draft, template AND all instances
```

URL aliases give a flow's webhook a stable, human-readable public URL that
survives flow re-creation. `invoke` fires it without authentication — handy in
E2E tests:

```sh
appmixer url-alias create -d '{"alias":"new-lead","flowId":"...","componentId":"..."}'
appmixer url-alias ls --json
appmixer url-alias get|update|rm <aliasId>
appmixer url-alias invoke <aliasId> new-lead -X POST -d '{"email":"a@b.c"}'
```

Automation Hub — the public template marketplace. Settings (appearance,
behavior, access control) are admin-only; analytics tracks template usage:

```sh
appmixer automation-hub analytics --json
appmixer automation-hub settings get --json
appmixer automation-hub settings set -d '{"title":"Acme Hub", ...}'
appmixer automation-hub settings rm
```

The e-mail/OTP shareable-link endpoints are browser-interactive and are
available in the API client (`src/api/automation-hub.js`) only.

### test

Dev tools for testing components and authentication before publishing:

- `appmixer test auth <authModuleFile>` – test an authentication module (`auth.js`).
- `appmixer test auth validate <authModuleFile>` – check that an access token is still accepted,
  without running the whole consent flow (`-a`, `-c`, `-s`, `-n`).
- `appmixer test auth refresh <authModuleFile>` – exercise the module's OAuth2 refresh
  (`-a`, `--refreshToken`, `-c`, `-s`, `-n`, `-r`).
- `appmixer test component <componentDir>` – run a component locally (`receive`/`tick`) with real API calls.
- `appmixer test component <componentDir> --test` – invoke a trigger's `test()` method (Flow Test
  Mode semantics: start/tick/receive are skipped, the sample item is emitted via
  `context.sendJson`) — verifies Flow Test Mode support without publishing to an instance.
- `appmixer test dump <moduleName>` – print stored authentication data from previous commands.
- `appmixer test flow <command>` – E2E test flow utilities.
- `appmixer test flow generate [name]` – scaffold an E2E test flow JSON from the local template.

### modifiers

Modifiers are the small transformation functions offered in the Designer's
variable picker (uppercase, date formatting, ...). Management requires admin
privileges:

```sh
appmixer modifiers get -o modifiers.json    # download { categories, modifiers } to a file
appmixer modifiers publish modifiers.json   # replace the modifier set from a file
appmixer modifiers restore                  # publish the built-in default set
appmixer modifiers delete                   # delete all (defaults return on engine restart)

appmixer modifiers test -d '{"code":"...","value":"hello"}' --json      # test an implementation
appmixer modifiers transform -d '{"value":"hello","modifiers":[...]}'   # apply modifiers to a value
```

### transfer

Transfers flows and related resources (users, accounts, config) between two Appmixer instances. See [README_TRANSFER.md](README_TRANSFER.md) for the full guide.

```sh
appmixer transfer config          # configure source/target URLs and tokens
appmixer transfer flow            # transfer flows between instances
appmixer transfer flow details <flowId>   # Appmixer version and connectors a flow depends on
```

## Alias Storage

On macOS and Linux, Appmixer CLI stores API URL aliases and sessions in
`~/.config/configstore/appmixer.json` using the `configstore` package (mode `0600`).
Aliases live under `appmixer-url`, one session per instance under `sessions`, and the
active session — the one every command uses — at the top level:

```json
{
  "appmixer-url": {
    "default": { "url": "https://api.appmixer.com", "alias": "default" },
    "aliases": { "dev": { "url": "http://localhost:2200", "alias": "dev" } }
  },
  "sessions": {
    "https://api.appmixer.com": { "user": {}, "token": "…", "context": null, "obtainedAt": "…" },
    "http://localhost:2200": { "user": {}, "token": "…", "context": null, "obtainedAt": "…" }
  },
  "user": {},
  "token": "…",
  "context": null
}
```

`appmixer alias <alias>` copies that instance's session into the active slot; `context` is
the workspace the active token is scoped to (absent in the personal context). A session
written by a CLI older than per-instance sessions is adopted for the current URL on first
use, so upgrading does not log you out.

The tokens are stored in plain text. Treat the file the way you treat
`~/.aws/credentials`: `appmixer logout --all` before handing the machine on, and prefer
`APPMIXER_TOKEN` (from a secret store) in CI over a checked-in session.

You can view the file with:

```sh
cat ~/.config/configstore/appmixer.json
```

Or pretty-print with `jq`:

```sh
jq . ~/.config/configstore/appmixer.json
```

## Development

### Repository layout

```
appmixer                  # the CLI entrypoint (commander git-style dispatch)
appmixer-<cmd>[-<sub>].js # one file per (sub)command; hubs declare subcommands
src/command.js            # runCommand(spec) factory — owns --json, client creation,
                          # error handling, help/examples and the parse tail
src/api/                  # the unified Appmixer API client layer: client.js (auth,
                          # ApiError mapping) + one module per resource (flows, store,
                          # accounts, logs, ...). ALL HTTP goes through this layer —
                          # an invariant test (test/api/griddriver-migration.test.js)
                          # enforces it.
src/sso-login.js          # browser SSO flow behind `appmixer login --sso`
src/session.js            # sessions: one per instance URL + the active slot every
                          # other module reads, and the workspace context the
                          # active token is scoped to
src/connector-ref.js      # connector reference utility (appmixer:google:gdrive ↔
                          # src/appmixer/google/gdrive ↔ component-type prefixes)
src/validators/           # the local flow validator suite (29 rules, ESM island)
src/e2e-runner/           # appmixer e2e internals: runner.js (run/watch core),
                          # orchestrator.js (--fix triage loop), importer.js,
                          # exporter.js, variables-check.js, triage.js (ESM island)
src/ai/                   # AI connector/component generator (see below)
dist/                     # bundled engine runtime for `appmixer test component`
```

### Adding a command

Create `appmixer-<hub>-<name>.js` on the `runCommand` factory, register it in the
hub file (`appmixer-<hub>.js`) — and for a new hub, add one line to the root
`appmixer` file:

```js
'use strict';
const api = require('./src/api');
const { runCommand } = require('./src/command');

runCommand({
    name: 'appmixer thing get',
    args: '<thingId>',
    run: ({ client, args }) => api.things.getThing(client, args.thingId)
});
```

The factory registers `--json` and `--workspace <name|id>`, creates the authenticated client
(switching into the workspace when asked), prints the returned value, maps errors, and
handles the missing-argument help. Custom pretty output goes in
a `print:` callback; commands with their own exit-code contract call `process.exit`
inside `run`.

### Tests

```sh
npm test          # openapi generator tests
npm run test-cli  # transfer + TLS suites
npm run test-api  # API layer, commands (spawned against a fake gridd server),
                  # validators, e2e-runner — see test/api/support/fake-gridd.js
npm run test-ai   # AI generator suites
npm run test-all  # everything
```

Command-level tests spawn the real entrypoints against `fake-gridd` (a minimal in-process
HTTP server that records every request), so they pin argument parsing, `--json` output
shape and exit codes end to end.

# Appmixer AI Connector & Component Generator (deprecated)

> **⚠️ DEPRECATED.** The `appmixer ai` commands are superseded by
> [appmixer-skills](https://github.com/Appmixer-ai/appmixer-skills) — agent skills
> (`build-connector`, `test-connector`, `review-connector`) that drive this CLI from
> Claude Code, Cursor and other AI coding agents to scaffold, test, review and publish
> connectors, including E2E flow testing via the `appmixer e2e` commands. Use the skills
> for new work; the commands below remain for backward compatibility and will be removed
> in a future release.

An AI-powered CLI tool that automatically generates complete Appmixer connectors and components. This tool leverages AI to analyze API documentation and create production-ready connector code with proper authentication, components, and test plans.

> Important: This is an experimental tool and may contain bugs or produce incomplete results.

## Table of Contents

- [Quick Start](#quick-start)
- [Installation](#installation)
- [Commands](#commands)
    - [Generate Connector](#generate-connector)
    - [Generate Component](#generate-component)
- [How It Works](#how-it-works)

---

## Quick Start

```bash
# 1. Clone and setup
git clone https://github.com/appmixer-ai/appmixer-connectors.git
cd appmixer-connectors
npm install appmixer

# 2. Configure environment (create .env in appmixer-connectors root)
copy .env.example .env   # Windows
# Edit .env and add your API keys (OpenAI, Anthropic, etc.)

# 3. Generate a connector
appmixer ai connector stripe --context ./stripe-context.md --icon ./stripe.svg

# 4. Generate a component
appmixer ai component stripe CreateCharge

```

---

## Installation

### Prerequisites
- Node.js v16 or higher
- API keys for AI services (OpenAI, Anthropic, etc.)

### Setup Steps

1. Clone the connectors repository:
```bash
git clone https://github.com/appmixer-ai/appmixer-connectors.git
cd appmixer-connectors
```

2. Install the Appmixer CLI:
```bash
npm install appmixer
```

3. Create `.env` file in the appmixer-connectors root:
```bash
copy .env.example .env   # Windows
# cp .env.example .env   # macOS/Linux
```

Edit `.env` and add your API keys:
```env
OPENAI_API_KEY=your_key_here
ANTHROPIC_API_KEY=your_key_here
```

---

## Commands

### Generate Connector

Creates a complete connector with authentication, components, and test plans.

```bash
appmixer ai connector <connector-name> [options]
```

**Options:**
- `-i, --icon <file>` - **Required for the first run.** Path to icon file (embedded in service.json as data URI).
- `-c, --context <file>` - Optional. Path to Markdown/text API docs
- `-m, --module <name>` - Module name (default: "core")
- `-e, --vendor <name>` - Vendor namespace (default: "appmixer")


**Context Resolution (if --context not provided):**
1. Uses OpenAPI spec at `src/appmixer/<connector>/artifacts/openapi.json` (if exists)
2. Attempts automatic discovery via web search

**Context file example**
Context file includes basic information about the API and expected components to be generated.

sample context file for Harvest API:
```
# Harvest Connector for Appmixer

## Overview
Harvest connector provides integration with Harvest's API v2 for time tracking, project management, invoicing, and expense management. Harvest is a time tracking and invoicing software used by businesses and freelancers.

## Authentication
- **Type**: OAuth 2.0
- **Authorization URL**: `https://id.getharvest.com/oauth2/authorize`
- **Token URL**: `https://id.getharvest.com/api/v2/oauth2/token`
- **Required Headers**: 
  - `Authorization: Bearer ACCESS_TOKEN`
  - `Harvest-Account-ID: ACCOUNT_ID`
  - `User-Agent: APPLICATION_NAME (contact@email.com)`
- **Setup**: [Harvest Developers Portal](https://id.getharvest.com/developers)

## Components

### Client Management
- **ListClients** - Retrieve all clients with active/inactive filtering
- **GetClient** - Retrieve specific client by ID
- **CreateClient** - Create new client
- **UpdateClient** - Update client details
- **DeleteClient** - Archive client (soft delete)
- **ListContacts** - List client contacts
- **CreateContact** - Create client contact
- **UpdateContact** - Update contact information
- **DeleteContact** - Remove client contact

```

**Examples:**
```bash
# With context file
appmixer ai connector stripe \
  --context "./stripe-api-docs.md" \
  --icon "./stripe.svg"

# Without context (uses OpenAPI or auto-discovery)
appmixer ai connector stripe --icon "./stripe.svg"
```

**What Gets Generated:**
- `auth.js` - Authentication module.
- `service.json` - Service metadata, base URLs, icon
- `bundle.json` - Component registration
- `components/` - One directory per component with `component.json` and `<Name>.js`
- `<connector>/artifacts/ai-artifacts` - generator artifacts and logs. This folder contains intermediate files used by the AI generator for reproducibility and debugging. Context file (if provided) and icon file is copied here.

---
#### How It Works

#####  Generation Workflow

1. ✅ **Detect or generate auth.js**
2. ✅ **Detect or generate components**
3. ✅ **Apply Appmixer standards (refactor)** - Applied for all components. Static analysis of the generated code. Apply fixes to follow Appmixer conventions.
4. ✅ **Validate authentication** - Test the authentication using the Appmixer CLI command `appmixer test auth login <path to auth.js>`. If it fails, prompt user to fix or continue. Authentication is required for further steps.
5. ✅ **Detect or create test plan** - Test plan is logical sequence for component tests. For example "Create" component must be tested before "Get" or "Update".
6. ✅ **Generate tests and test components** - For each component in the test plan, generate test cases and run tests (using the `appmixer test component` command). Connector is marker as complete when at least one test case per component passes. There is also a limit of 5 attempts to fix failed tests. After 5 attempts, the process stops and component is marked as failed.
7. ✅ **Report results** - Summary of test results - `test-plan-report.md` is generated in the connector artifacts folder.

**Important Note**: You can interrupt the process at any time (Ctrl+C). The generated code up to that point will be saved, along with logs and artifacts for debugging. When you re-run the command, it will resume from the last successful step. This allows you to fix any issues (e.g., authentication) and continue without starting over.

### Generate Component

Creates individual components for an existing connector.

```bash
appmixer ai component <connector> <component>
```

**Arguments:**
- `<connector>` - Required. Connector name
- `<component>` - Required. Component name

**Options:**
- `-m, --module <name>` - Module name (default: "core")
- `-e, --vendor <name>` - Vendor namespace (default: "appmixer")

**Examples:**
```bash
# Single component
appmixer ai component stripe CreateCharge
```

**Note:** Component commands must be run from the `appmixer-connectors` root directory.

---

## Artifacts System

AI generates intermediate artifact files (JSON) stored in `<connector>/artifacts/ai-artifacts/`:

- **`<component>/componentRecipe.json`** - Component metadata, schemas, sample data (generated when `appmixer ai component` is run)
- **`SERVICE_INFO_ACTIONS_AND_TRIGGERS`** - Connector metadata, schemas, sample data for all components (generated when `appmixer ai connector` is run)
- **`context.md`** - Copy of your context file for reproducibility
- **`commands-log`** - Text file containing all CLI commands run during generation
- **`testplan.json`** - Component test cases and progress
- **`progress.json`** - JSON file tracking progress of the refactoring steps (Apply Appmixer standards step).

---

**Force regeneration:**
```bash
rm -rf ai-artifacts/<connector>  # Delete artifacts
appmixer ai connector <connector> --context ./context.md --icon ./icon.svg
```

# Flow Transfer Command
The `appmixer transfer flow` command allows you to transfer a flow (draft) from one Appmixer instance to another, including updating the associated template and all existing integration instances. This is useful for syncing integrations across development, staging, and production environments.

The transfer process works in three sequential steps: first, it syncs the draft (blueprint) from the source to the target instance; second, it updates the associated template if one exists on the target; and third, it optionally refreshes all integration instances based on the updated template. You can control which steps to execute using flags like `--update-instances` to include instance updates, or skip certain steps entirely.

Configuration can be managed interactively using `appmixer transfer config -i` to save source and target URLs with authentication tokens for future transfers. Alternatively, you can provide credentials via command-line flags or environment variables.

Artifact import/export — the transfer command can now save and import flow artifacts. Use `--artifact-path <path>` during export to save draft/template/details to disk. To import flows from saved artifacts (instead of connecting to a source instance) use `--from-artifacts <path>` — flows are discovered by scanning subdirectories under the artifact path (valid flow directories contain `details.json`). When using `--from-artifacts` you do not need `--source-url` or `--source-token` and some flags are incompatible with artifact import (see [README_TRANSFER.md](README_TRANSFER.md) for full guidance and examples).

The command supports batch transfers via `--flow-list-file` (up to 100 flows), and includes safety features like `--dry-run` mode to preview changes before applying them.

The command also supports multi-target transfers (push to multiple target environments) using `--target-list-file <path>`, where `<path>` points to a JSON file with an array of target objects (each with `url` and `token`); see [README_TRANSFER.md](README_TRANSFER.md) for examples and limits (maximum 10 targets).
