diff --git a/CLAUDE.md b/CLAUDE.md index c18874a..43674ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`) | @@ -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 ` command is + supplied on standard input, so the secret is never a `-w ` 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 ` | n8n instance URL | -| `--api-key ` | API key (overrides keychain) | -| `--config ` | 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 ` | n8n instance URL | +| `--api-key ` | API key for the n8n public API | +| `--config ` | 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 @@ -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: ''`). `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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 908107a..ae60bb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/SECURITY.md b/SECURITY.md index 49e90f4..200c710 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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. diff --git a/docs/community-vs-enterprise.md b/docs/community-vs-enterprise.md index 36be364..ffb1792 100644 --- a/docs/community-vs-enterprise.md +++ b/docs/community-vs-enterprise.md @@ -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 @@ -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 @@ -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 diff --git a/docs/guides/ai-agents.md b/docs/guides/ai-agents.md index e8d74e2..8d31816 100644 --- a/docs/guides/ai-agents.md +++ b/docs/guides/ai-agents.md @@ -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 diff --git a/docs/guides/folders.md b/docs/guides/folders.md index 230b7b2..60fd608 100644 --- a/docs/guides/folders.md +++ b/docs/guides/folders.md @@ -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 @@ -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 diff --git a/docs/guides/workflow-lifecycle.md b/docs/guides/workflow-lifecycle.md index 74f8951..b621e62 100644 --- a/docs/guides/workflow-lifecycle.md +++ b/docs/guides/workflow-lifecycle.md @@ -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 diff --git a/docs/reference/auth.md b/docs/reference/auth.md index 18835bd..6a898ad 100644 --- a/docs/reference/auth.md +++ b/docs/reference/auth.md @@ -12,6 +12,10 @@ Credentials are stored per URL, in the keychain service `8cli`, under the accoun `/api-key`, `/email` and `/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 ` command to `security -i` on +standard input. The secret is encoded as hexadecimal data in that command, never passed as a +`-w ` 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 diff --git a/docs/reference/folder.md b/docs/reference/folder.md index e53c759..209d100 100644 --- a/docs/reference/folder.md +++ b/docs/reference/folder.md @@ -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. @@ -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": "", + "workflowName": "New order webhook", + "toFolder": "(root)" + } } ``` diff --git a/docs/reference/source-control.md b/docs/reference/source-control.md index c15760a..a6a2d2b 100644 --- a/docs/reference/source-control.md +++ b/docs/reference/source-control.md @@ -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 ] ``` -No options. +| Option | Required | Meaning | +| -------------------------- | -------- | ------------------------------------------------- | +| `--direction ` | 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 @@ -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`. diff --git a/docs/reference/workflow.md b/docs/reference/workflow.md index be1ad8f..91f6a9b 100644 --- a/docs/reference/workflow.md +++ b/docs/reference/workflow.md @@ -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. diff --git a/docs/runbooks/local-n8n.md b/docs/runbooks/local-n8n.md index efb9bf8..099179a 100644 --- a/docs/runbooks/local-n8n.md +++ b/docs/runbooks/local-n8n.md @@ -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 @@ -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 diff --git a/docs/security.md b/docs/security.md index de0688b..9541880 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 -U +-X ` command on standard input. The secret is hex-encoded in that input; it is never passed as +a `-w ` process argument. + ## What 8cli prints - `config show` masks the API key (first four and last four characters, for example @@ -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). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7c09cfd..54c6776 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -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).