Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ AI-first n8n remote management CLI. JSON output by default, no interactive promp

| Detail | Value |
| ---------------- | ---------------------------------------------------------------------------------- |
| **Runtime** | Node.js 22+ (native fetch) |
| **Runtime** | Node.js 22.22+ (native fetch) |
| **Language** | TypeScript (strict, nodenext) |
| **Package** | `@qodeca/8cli` (scoped npm); CLI command stays `8cli` |
| **Entry point** | `bin/8cli.ts` (dev via `tsx`); built to `dist/bin/8cli.js` (`#!/usr/bin/env node`) |
Expand Down Expand Up @@ -99,20 +99,22 @@ Secrets are **never** stored in config files. They live in the OS keychain:

- **Service name**: `8cli`
- **Account names**: `{url}/api-key`, `{url}/email`, `{url}/password`
- **macOS**: `security add-generic-password` / `find-generic-password` / `delete-generic-password`
- **macOS**: stores through `security -i`: an `add-generic-password -U -X <hex>` command is
supplied on standard input, so the secret is never a `-w <secret>` process argument; reads
use `find-generic-password -w` and deletes use `delete-generic-password`
- **Windows/Linux**: stubs – not yet implemented

## Global options

| Flag | Purpose |
| ----------------- | -------------------------------- |
| `--url <url>` | n8n instance URL |
| `--api-key <key>` | API key (overrides keychain) |
| `--config <path>` | Config file path |
| `--table` | Human-readable table output |
| `--dry` | Preview changes without applying |
| `--verbose` | Debug logging to stderr |
| `--insecure` | Allow plaintext-HTTP URLs |
| Flag | Purpose |
| ----------------- | ------------------------------------------------------------ |
| `--url <url>` | n8n instance URL |
| `--api-key <key>` | API key for the n8n public API |
| `--config <path>` | Config-file path (default: auto-detect) |
| `--table` | Output a table instead of JSON |
| `--dry` | Preview changes without applying |
| `--verbose` | Enable verbose logging to stderr |
| `--insecure` | Allow plaintext HTTP URLs; the API key travels in clear text |

## n8n API gotchas

Expand All @@ -121,7 +123,7 @@ These are already handled in the code but important to know:
1. `PUT /workflows/{id}` rejects extra fields – only send: `name`, `nodes`, `connections`, `settings`, `staticData`
2. `active` is read-only on PUT – always strip from publish payload
3. Settings accepts n8n's known keys (`executionOrder`, `timezone`, `saveDataSuccessExecution`, `errorWorkflow`, …) – the schema is strict, so only an unknown key is rejected (`Unrecognized key(s) in object: '<key>'`). `publish` keeps the known keys and drops the unknown ones
4. Public API doesn't expose folder info – internal API (cookie auth) needed for folders
4. Folder commands use the internal API (cookie auth), not the public API
5. Execution data needs `?includeData=true` query param on GET

## Two API clients
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ added in the future.)

## Prerequisites

- Node.js **22+** (the CLI uses native `fetch`)
- Node.js **22.22+** (the CLI uses native `fetch`)
- npm

## Local setup
Expand Down
5 changes: 3 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@ organisation. This is a last resort. It is not a second place to report.

## What this project promises

Secrets are never written to config files. They live only in the OS keychain. The CLI never
prompts, and errors go to stderr as JSON. HTTPS is the default.
Secrets are never written to config files. On macOS, 8cli can store them in the OS keychain;
callers can also provide them through environment variables. The CLI never prompts, errors go to
stderr as structured JSON, and HTTPS is the default.
56 changes: 18 additions & 38 deletions docs/community-vs-enterprise.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ it and 8cli reports the refusal as a normal error.

Tested against n8n 2.40.5 Community with no license.

| Group | Community | What you see on Community |
| ---------------- | --------- | ------------------------------------------------------------------------------------------ |
| `auth`, `config` | Yes | Works (these are local; `auth verify` calls n8n) |
| `wf` | Yes | Works |
| `exec` | Yes | Works |
| `cred` | Yes | Works; `cred transfer` needs a second project, which needs a license |
| `tag` | Yes | Works |
| `user` | Yes | Works |
| `dt` | Yes | Works |
| `audit` | Yes | Works |
| `var` | **No** | `Your license does not allow for feat:variables…` |
| `proj` | **No** | `Your license does not allow for feat:projectRole:admin…` |
| `folder` | **No** | `Plan lacks license for this feature` |
| `sc` | **No** | `sc status`: `not found`; `sc pull`: `Your license does not allow for feat:sourceControl…` |
| Group | Community | What you see on Community |
| ---------------- | --------- | -------------------------------------------------------------------- |
| `auth`, `config` | Yes | Works (these are local; `auth verify` calls n8n) |
| `wf` | Yes | Works |
| `exec` | Yes | Works |
| `cred` | Yes | Works; `cred transfer` needs a second project, which needs a license |
| `tag` | Yes | Works |
| `user` | Yes | Works |
| `dt` | Yes | Works |
| `audit` | Yes | Works |
| `var` | **No** | `Your license does not allow for feat:variables…` |
| `proj` | **No** | `Your license does not allow for feat:projectRole:admin…` |
| `folder` | **No** | `Plan lacks license for this feature` |
| `sc` | **No** | `Your license does not allow for feat:sourceControl…` |

## What the refusals look like

Expand Down Expand Up @@ -67,28 +67,9 @@ code; the message is n8n's.
}
```

**Source control.** `sc status` does not mention a license. n8n answers `not found`, because
the source-control endpoint is not there without the feature:

```bash
8cli sc status
```

```json
{
"error": "not found",
"code": "ERR_SOURCE_CONTROL"
}
```

`sc pull` names the license:

```json
{
"error": "Your license does not allow for feat:sourceControl. To enable feat:sourceControl, please upgrade to a license that supports this feature.",
"code": "ERR_SOURCE_CONTROL"
}
```
**Source control.** `sc status` and `sc pull` fail with `ERR_SOURCE_CONTROL` and an error message
that names the license. On n8n 2.40.5 `sc pull` answered
`Your license does not allow for feat:sourceControl. …`.

## Telling a license refusal from a real failure

Expand All @@ -103,8 +84,7 @@ that command. In a script:
}
```

`sc status` on Community says only `not found`; treat `not found` from `sc status` as "source
control is not available here".
`sc status` and `sc pull` give the normal source-control license refusal on Community.

## Licensed editions

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/ai-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ starting point:
- Never pass an API key or password on the command line; they are in the environment.
- `wf diff` prints plain text when files differ. `wf publish` exits 0 even when its `errors`
array is not empty – always check it.
- `var`, `proj`, `folder` and `sc` need a licensed n8n; a `license` or `not found` error from
them is not a bug.
- `var`, `proj`, `folder` and `sc` need a licensed n8n; a license refusal from them is not a
bug.
```

## Patterns that work
Expand Down
12 changes: 6 additions & 6 deletions docs/guides/folders.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ n8n has two APIs:
| **Public API** | `/api/v1/...` | An API key (`X-N8N-API-KEY`) | Yes, it is documented | Everything but folders |
| **Internal API** | `/rest/...` | Email and password → a session cookie | No, it serves n8n's own web app | `folder` commands only |

n8n's public API has no folders at all. The only way to read or change them is the internal API
that n8n's own web app uses. So the `folder` commands log in exactly like a browser does: they
send your email and password to `/rest/login`, keep the session cookie, and use it for the
folder requests.
8cli's folder implementation uses the internal API that n8n's own web app uses. The `folder`
commands log in exactly like a browser does: they send your email and password to `/rest/login`,
keep the session cookie, and use it for the folder requests.

## What that means for you

Expand Down Expand Up @@ -84,8 +83,9 @@ output is not shown here; the shapes are on the [`folder` reference](../referenc
```

Folders and workflows are found by **name**, ignoring upper and lower case. Quote names that
contain spaces. `--to '(root)'` moves a workflow out of every folder (on n8n 2.40.5 Community it
fails with `Expected string, received null`).
contain spaces. `--to '(root)'` moves a workflow out of every folder. Since 8cli 0.2.0 it sends
n8n's root marker (`"0"`) and n8n accepts it, Community included; before, it failed with
`Expected string, received null`.

## Mirroring folders on disk

Expand Down
5 changes: 3 additions & 2 deletions docs/guides/workflow-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,9 @@ Then publish:
Without `--id`, `wf publish` sends **every** file in the folder. **Always read `errors`**: a file
n8n rejects is listed there and the exit code is still 0.

What publish sends: the name, nodes, connections, static data and the `executionOrder` setting.
It does not change whether the workflow is active, and it does not send tags or other settings.
What publish sends: the name, nodes, connections, static data and every settings key that n8n
accepts. It drops unknown settings, does not change whether the workflow is active, and does not
send tags.

## 5. Save again after publishing

Expand Down
4 changes: 4 additions & 0 deletions docs/reference/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Credentials are stored per URL, in the keychain service `8cli`, under the accoun
`<url>/api-key`, `<url>/email` and `<url>/password`. The URL comes from the global `--url` or
`N8N_URL` – not from `8cli.json`.

On macOS, storage sends an `add-generic-password -U -X <hex>` command to `security -i` on
standard input. The secret is encoded as hexadecimal data in that command, never passed as a
`-w <secret>` process argument.

**Reading a secret from standard input.** `--value`, `--password` and the global `--api-key`
accept `-` in these commands: 8cli then reads the secret from standard input and strips one
trailing newline. Using stdin avoids putting the literal secret in the command you type. It works
Expand Down
20 changes: 15 additions & 5 deletions docs/reference/folder.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
Shows and changes the folders workflows live in, and mirrors them into your local workflow
files.

> **Uses n8n's internal API, with a password.** n8n's public API has no folders, so these
> commands log in the way the n8n web app does, with an email and password. Set `N8N_EMAIL` and
> **Uses n8n's internal API, with a password.** These commands log in the way the n8n web app
> does, with an email and password. Set `N8N_EMAIL` and
> `N8N_PASSWORD` (or, on macOS, `auth set-credentials`). The API key is not used. Read
> [folder commands and the internal API](../guides/folders.md) before you rely on them.

Expand Down Expand Up @@ -171,12 +171,22 @@ On Community, with a folder name:
}
```

With `--to '(root)'`, n8n 2.40.5 Community answers with a validation error instead:
With `--to '(root)'` 8cli skips the folder lookup and sends n8n's root marker (`"0"`), which n8n
accepts on Community too. Before 8cli 0.2.0 this failed with `Expected string, received null`.
On Community this was checked with a workflow already at the root; taking a workflow out of a real
folder needs a licensed n8n and was not run for these docs.

```bash
8cli folder move 'New order webhook' --to '(root)'
```

```json
{
"error": "Expected string, received null",
"code": "ERR_FOLDER_MOVE"
"moved": {
"workflowId": "<id>",
"workflowName": "New order webhook",
"toFolder": "(root)"
}
}
```

Expand Down
49 changes: 16 additions & 33 deletions docs/reference/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,37 @@
Reads the status of n8n's Git-based source control and pulls changes into n8n. Pushing is not
possible through n8n's public API.

> **Needs a licensed n8n with source control set up.** On the Community edition `sc status`
> answers `not found` and `sc pull` names the missing license, as shown below. The success
> **Needs a licensed n8n with source control set up.** On the Community edition `sc status` and
> `sc pull` fail with `ERR_SOURCE_CONTROL` and an error message that names the license. The success
> output is n8n's own response, passed through; it was not run for these docs. See
> [Community and Enterprise n8n](../community-vs-enterprise.md).

| Subcommand | What it does |
| ------------------------- | ----------------------------------------------- |
| [`sc status`](#sc-status) | Show the source-control settings |
| [`sc status`](#sc-status) | List the changes waiting to be pulled or pushed |
| [`sc pull`](#sc-pull) | Pull the connected Git branch into n8n |
| [`sc push`](#sc-push) | Always fails: not supported by n8n's public API |

## sc status

```text
8cli sc status
8cli sc status [--direction <pull|push>]
```

No options.
| Option | Required | Meaning |
| -------------------------- | -------- | ------------------------------------------------- |
| `--direction <pull\|push>` | No | Which changes to list: `pull` (default) or `push` |

**Output:** n8n's source-control preferences object, passed through (the connected repository,
branch and similar settings).
Calls n8n's `GET /api/v1/source-control/status` with the chosen direction. An invalid
`--direction` value fails with `ERR_USAGE` before any request is sent.

On Community n8n answers `not found`, without mentioning a license:
**Output:** n8n's status response, passed through: the list of changes that a pull (or a push)
would move between n8n and the connected Git branch.

```bash
8cli sc status
```
On Community, `sc status` (with either direction) fails with `ERR_SOURCE_CONTROL` and an error
message that names the license.

```json
{
"error": "not found",
"code": "ERR_SOURCE_CONTROL"
}
```

**Errors:** `ERR_SOURCE_CONTROL`.
**Errors:** `ERR_SOURCE_CONTROL`, `ERR_USAGE`.

## sc pull

Expand All @@ -56,20 +51,8 @@ Ignores `--dry`.

**Output:** n8n's pull result, passed through.

On Community:

```bash
8cli sc pull
```

```json
{
"error": "Your license does not allow for feat:sourceControl. To enable feat:sourceControl, please upgrade to a license that supports this feature.",
"code": "ERR_SOURCE_CONTROL"
}
```

`8cli sc pull --force` returns the same.
On Community, `sc pull` and `sc pull --force` fail with `ERR_SOURCE_CONTROL` and an error message
that names the license.

**Errors:** `ERR_SOURCE_CONTROL`.

Expand Down
6 changes: 3 additions & 3 deletions docs/reference/workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,9 @@ it; any other file **creates** a new workflow, and n8n gives it a new ID.
With neither option, it publishes every `.json` file in `workflowDir`. `--file` wins over `--id`.
There is no `--dir`: set `workflowDir` in `8cli.json` if your files live elsewhere.

What is sent: only `name`, `nodes`, `connections`, `staticData`, and `settings.executionOrder`.
Everything else in the file (`active`, `tags`, other settings, `id`, dates) is left out, because
n8n's API refuses it. Publishing does not activate or deactivate a workflow.
What is sent: `name`, `nodes`, `connections`, `staticData`, and `settings` keys that n8n accepts.
Unknown settings keys, and all other file fields (`active`, `tags`, `id`, dates), are left out
because n8n's API rejects them. Publishing does not activate or deactivate a workflow.

Honours `--dry`: reports what it would update and create, without sending anything.

Expand Down
7 changes: 4 additions & 3 deletions docs/runbooks/local-n8n.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ source control) answer with the gated error, not as working features.
- Image: `n8nio/n8n:2.40.5`, pinned by digest in `scripts/local-n8n/compose.yaml` (the same pin
as the e2e suite in `test/e2e/setup/global.ts`).
- URL: `http://localhost:5678`, bound to loopback only. Set `N8N_LOCAL_PORT` to use another port.
- Needs: Docker with Compose, Node.js 22+, `npm install` done.
- Needs: Docker with Compose, Node.js 22.22+, `npm install` done.

## Start

Expand Down Expand Up @@ -42,8 +42,9 @@ npx tsx bin/8cli.ts wf list
```

`--store env` is accepted and is the default. `--store keychain` is refused with
`ERR_STORE_DISABLED`: the keychain store is disabled until the keychain backend keeps secrets out
of process arguments.
`ERR_STORE_DISABLED` and the message "The keychain store is disabled until the keychain backend
keeps secrets out of process arguments; use the env file store (--store env, the default)". The
helper supports environment-file storage only.

### Moving from a per-checkout file

Expand Down
8 changes: 6 additions & 2 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ To report a vulnerability in 8cli, follow the [security policy](../SECURITY.md).
- **The macOS keychain** – service `8cli`, one entry per URL. See
[configuration](configuration.md#the-macos-keychain).

On macOS, 8cli runs `security -i` and sends it an `add-generic-password -s 8cli -a <account> -U
-X <hex>` command on standard input. The secret is hex-encoded in that input; it is never passed as
a `-w <secret>` process argument.

## What 8cli prints

- `config show` masks the API key (first four and last four characters, for example
Expand Down Expand Up @@ -63,8 +67,8 @@ you control, and prefer putting n8n behind HTTPS.

## Folder commands use a password

Folder commands cannot use the API key, because n8n does not expose folders in its public API.
They log in to n8n's internal API with an email and password, like the browser does. That
Folder commands use n8n's internal API rather than the public API. They log in with an email and
password, like the browser does. That
password gives full user access, not a scoped API key. Use a dedicated n8n user for automation
where you can. See [folder commands and the internal API](guides/folders.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ The status must be one of n8n's values: `canceled`, `crashed`, `error`, `new`, `

## Licensed features

### `Your license does not allow for feat:…`, `Plan lacks license for this feature`, or `not found` from `sc status`
### `Your license does not allow for feat:…` or `Plan lacks license for this feature`

Your n8n is the Community edition. Variables, projects, folders and source control need a
licensed n8n. See [Community and Enterprise n8n](community-vs-enterprise.md).
Expand Down
Loading