n8n-decanter 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/README.md +89 -46
- package/dist/lib/api.mjs +43 -0
- package/dist/lib/config.mjs +2 -0
- package/dist/lib/datatables.mjs +124 -0
- package/dist/lib/engine.mjs +192 -0
- package/dist/lib/executions.mjs +21 -2
- package/dist/lib/init.mjs +4 -4
- package/dist/lib/picker.mjs +9 -3
- package/dist/lib/pull.mjs +23 -22
- package/dist/lib/rename.mjs +11 -2
- package/dist/lib/simulate.mjs +374 -0
- package/dist/lib/state.mjs +10 -6
- package/dist/lib/util.mjs +4 -0
- package/dist/n8n-decanter.mjs +253 -81
- package/package.json +3 -2
- package/template/.claude/settings.local.json.example +16 -3
- package/template/.env.example +2 -0
- package/template/AGENTS.md.example +69 -28
- package/template/CLAUDE.md.example +1 -1
- package/template/n8n-globals.d.ts.example +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,130 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-07-21
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Breaking: verb-first grammar.** The verb now comes first —
|
|
15
|
+
`n8n-decanter <verb> [workflow…]`. Verb-last (`n8n-decanter wf123 push`) is no
|
|
16
|
+
longer accepted and errors with *unknown verb*. Because everything after the
|
|
17
|
+
verb is an argument, a workflow named like a verb needs no special handling:
|
|
18
|
+
`n8n-decanter status push` runs `status` on the workflow named `push`. Flags
|
|
19
|
+
may still appear in any position.
|
|
20
|
+
- **Breaking: node operations moved under a `node` namespace.** `add` →
|
|
21
|
+
`node create <workflow> "<Node name>"`, the two-name node rename →
|
|
22
|
+
`node rename <workflow> "<old node>" "<new node>"`, and `run <node-file>` →
|
|
23
|
+
`node run <node-file>`.
|
|
24
|
+
- New workflow folders are **kebab-case** (`Order Sync` → `workflows/order-sync/`)
|
|
25
|
+
instead of keeping spaces and capitals. **Existing folders are left untouched**
|
|
26
|
+
and still resolve as refs — no migration, no churn.
|
|
27
|
+
- A workflow folder **no longer follows a remote rename**. The folder is a stable
|
|
28
|
+
local slug; the always-current display name lives in `.decanter.json` (see
|
|
29
|
+
Added). Renaming a workflow (locally or on the server) never moves your folder.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`data-tables` verb** — a read-only fetch of n8n **data-table** schemas and
|
|
34
|
+
rows (the built-in project-scoped tables, n8n ≥ 2.x) into a top-level,
|
|
35
|
+
gitignored `data-tables/<table>/{meta,columns,rows}.json` dir, for developing
|
|
36
|
+
and debugging against real table contents offline. `--filter '<json>'`,
|
|
37
|
+
`--search`, and `--sort` pull only a slice of a large table server-side (the
|
|
38
|
+
applied filter is recorded in each table's `meta.json`); `--limit`/`--all`
|
|
39
|
+
control page size and exhaustion. It never writes a data table.
|
|
40
|
+
`data-tables clean` removes the dir (offline). Gated by the new **`dataTables`**
|
|
41
|
+
config key (default `true`); when off, the fetch refuses and the recommended
|
|
42
|
+
key needn't carry the data-table read scopes (`dataTable:list`,
|
|
43
|
+
`dataTable:read`, `dataTableColumn:read`, `dataTableRow:read`).
|
|
44
|
+
- `.decanter.json` now caches the workflow's display **`name`** (refreshed on
|
|
45
|
+
every pull), so the picker, `list`, and ref-resolution show the real name even
|
|
46
|
+
though the folder is a kebab slug — and keep working if `workflow.json` is
|
|
47
|
+
missing or corrupt.
|
|
48
|
+
- **`list --json`** emits `[{ name, id, dir }]` for tooling (remote-only
|
|
49
|
+
workflows under `--remote` have `dir: null`).
|
|
50
|
+
- **No-ref → picker.** A ref-taking verb given no workflow, on a terminal, opens
|
|
51
|
+
the interactive picker to choose one and runs the verb on it. Piped/non-TTY
|
|
52
|
+
runs keep the config-default / error behavior, so scripts and CI never block.
|
|
53
|
+
- **`simulate` now replays single-iteration loops.** A workflow whose only
|
|
54
|
+
repeated node is a `splitInBatches` ("Loop Over Items") driver that ran a
|
|
55
|
+
single batch — it runs twice (one batch pass + the final "done" pass) while
|
|
56
|
+
every other node ran once — no longer hard-errors. The loop driver executes
|
|
57
|
+
for real to reproduce the loop, and each node's one captured run pins exactly.
|
|
58
|
+
Multi-batch loops (any node ran more than once) stay out of scope, since
|
|
59
|
+
first-run-only pinning can't feed later iterations.
|
|
60
|
+
|
|
61
|
+
### Removed
|
|
62
|
+
|
|
63
|
+
- **Breaking: `rename --workflow` flag.** Workflow rename is now the single
|
|
64
|
+
top-level form `rename <workflow> "<new name>"`; node rename lives under
|
|
65
|
+
`node rename`.
|
|
66
|
+
|
|
67
|
+
## [0.4.5] - 2026-07-21
|
|
68
|
+
|
|
69
|
+
### Added
|
|
70
|
+
|
|
71
|
+
- **`simulate` in the interactive picker.** The verb menu for a pulled workflow
|
|
72
|
+
now offers `simulate` alongside status/pull/push/watch/check/executions; it
|
|
73
|
+
runs against the workflow's newest capture.
|
|
74
|
+
- **Open a simulation run in the n8n webapp.** In an interactive terminal,
|
|
75
|
+
`simulate` prints a **URL** to the run in a kept-alive local n8n (plus the
|
|
76
|
+
throwaway instance's login) — pure nodes' real output and the pinned nodes,
|
|
77
|
+
node-by-node in the actual execution inspector. No flag, no extra step; a
|
|
78
|
+
fresh viewer replaces the previous one each run (`docker rm -f
|
|
79
|
+
decanter-sim-viewer` to stop it). Scripts, `--json`, and `--network-none`
|
|
80
|
+
runs stay headless and print no URL, so CI is unaffected.
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- **`simulate` no longer requires `--execution`.** With the flag omitted it
|
|
85
|
+
defaults to the **newest capture** in the workflow's `executions/` dir, so
|
|
86
|
+
`n8n-decanter <ref> simulate` works right after an `executions` fetch (and
|
|
87
|
+
lets the picker offer it). Pass `--execution <id>` to pick a specific one.
|
|
88
|
+
|
|
89
|
+
## [0.4.4] - 2026-07-21
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- **The generated `.claude/settings.local.json` pre-approves more safe verbs.**
|
|
94
|
+
`rename`, `list`, `executions` (incl. `executions clean`), `completion`, and
|
|
95
|
+
`help` — plus a bare `status` — now run without a permission prompt, matching
|
|
96
|
+
the "offline, safe" and "reads remote, no writes" tiers documented in the
|
|
97
|
+
sync-dir `AGENTS.md`. Mutating/destructive verbs (`push`, `pull`, `watch`,
|
|
98
|
+
`publish`, `unpublish`, `create`, `duplicate`, `simulate`, `delete`) still
|
|
99
|
+
prompt, and `delete --force` is now hard-denied alongside `push --force`.
|
|
100
|
+
|
|
101
|
+
## [0.4.3] - 2026-07-21
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **`$('Node').item` in the type shim (`n8n-globals.d.ts`) is no longer typed
|
|
106
|
+
`| undefined`.** Accessing `$('Node').item.json` no longer raises a spurious
|
|
107
|
+
"Object is possibly 'undefined'" (TS2532) — the value is non-undefined, like
|
|
108
|
+
`$input.item`, since a missing paired item throws at runtime rather than
|
|
109
|
+
yielding `undefined`. Use `itemMatching(i)`, `first()`, or `last()` when you
|
|
110
|
+
want an index-checked lookup instead.
|
|
111
|
+
|
|
112
|
+
## [0.4.2] - 2026-07-20
|
|
113
|
+
|
|
114
|
+
### Added
|
|
115
|
+
|
|
116
|
+
- **`simulate` verb** — `n8n-decanter <ref> simulate --execution <id>` replays
|
|
117
|
+
a whole workflow through a **real n8n engine** (Docker) using a captured
|
|
118
|
+
execution as the mock: side-effect-free nodes (Set, IF, Code, …) execute for
|
|
119
|
+
real, every network/side-effectful node is pinned to its captured output,
|
|
120
|
+
credentials are stripped, and no outbound-capable node survives — a dry,
|
|
121
|
+
engine-true regression check. It diffs each executed node's output against the
|
|
122
|
+
capture and **exits `1` on divergence** (CI-gateable). `--network-none` adds
|
|
123
|
+
an enforced outbound cutoff; `--json` emits the report for tooling.
|
|
124
|
+
- **`simulate --pin <id>`** — copy a capture's network-node outputs into
|
|
125
|
+
committed, provenance-stamped `workflows/<Name>/fixtures/<node>.json`, making
|
|
126
|
+
replays reproducible and committable (prints a PII-review warning).
|
|
127
|
+
- **`n8nVersion` config field** (`decanter.config.json`) — pins the n8n version
|
|
128
|
+
the `simulate` engine runs, so "engine-true" matches your instance;
|
|
129
|
+
`--n8n-version <tag>` overrides it per run. Defaults to the project's pinned
|
|
130
|
+
version with a hint when unset.
|
|
131
|
+
- **`npm run test:sim`** — opt-in engine simulation suite (needs Docker; never
|
|
132
|
+
part of `npm test`); skips cleanly when no Docker daemon is available.
|
|
133
|
+
|
|
10
134
|
## [0.4.1] - 2026-07-20
|
|
11
135
|
|
|
12
136
|
### Changed
|
package/README.md
CHANGED
|
@@ -26,14 +26,19 @@ editable in your IDE or by your agent, and pushed back through the n8n API.
|
|
|
26
26
|
- **TypeScript or typed JS** — write nodes in TS (compiled on push); n8n
|
|
27
27
|
globals (`$input`, `$('…')`, …) are typed in both.
|
|
28
28
|
- **Agent-native** — `init` scaffolds Claude Code / Cursor / Codex configs
|
|
29
|
-
and verification hooks; offline `check` and `run` give agents a
|
|
29
|
+
and verification hooks; offline `check` and `node run` give agents a
|
|
30
30
|
credential-free feedback loop.
|
|
31
31
|
- **Guardrails** — a compliance guard and typecheck gate block broken
|
|
32
32
|
pushes; a drift guard keeps you from clobbering remote edits.
|
|
33
33
|
- **Real execution data on tap** — `executions` fetches recent run JSON into
|
|
34
34
|
a gitignored temp dir, so agents see actual payload shapes (and build
|
|
35
|
-
accurate `run` fixtures) instead of guessing; `executions clean` removes
|
|
35
|
+
accurate `node run` fixtures) instead of guessing; `executions clean` removes
|
|
36
36
|
it when done.
|
|
37
|
+
- **Engine-true simulation** — `simulate` replays a whole workflow through a
|
|
38
|
+
real n8n engine using a captured execution as the mock: pure nodes run for
|
|
39
|
+
real, network nodes pinned, credentials stripped. It diffs each node against
|
|
40
|
+
the capture (exits 1 on divergence — a CI-gateable regression check) and, in
|
|
41
|
+
a terminal, prints a URL to open the run in the n8n webapp.
|
|
37
42
|
- **Live editing** — `watch` pushes on save and auto-reloads the n8n editor
|
|
38
43
|
tab via a local proxy.
|
|
39
44
|
- **Shared code and small libraries** — `.ts` nodes import helpers/types
|
|
@@ -90,6 +95,8 @@ so a leaked `.env` has a small blast radius:
|
|
|
90
95
|
- `workflow:create`, `workflow:delete` — `create` / `delete`
|
|
91
96
|
- `workflow:activate`, `workflow:deactivate` — `publish` / `unpublish`
|
|
92
97
|
- `execution:read`, `execution:list` — the `executions` verb
|
|
98
|
+
- `dataTable:list`, `dataTable:read`, `dataTableColumn:read`, `dataTableRow:read`
|
|
99
|
+
— the `data-tables` verb (only needed while `"dataTables"` is on, its default)
|
|
93
100
|
|
|
94
101
|
A full-access key also works if your n8n version predates scoped keys.
|
|
95
102
|
|
|
@@ -105,7 +112,7 @@ PostToolUse hook that runs `check` after node edits. Verification routes through
|
|
|
105
112
|
the CLI, so `n8n-decanter` must be on the sync dir's PATH: install it globally
|
|
106
113
|
(`npm i -g n8n-decanter`), add it to the sync dir's `devDependencies`, or
|
|
107
114
|
`npm link` a git checkout (build it first — Node won't type-strip `.mts`
|
|
108
|
-
under `node_modules`). The verbs `check`, `run`, `rename`, and `
|
|
115
|
+
under `node_modules`). The verbs `check`, `node run`, `rename`, and `node create` are fully
|
|
109
116
|
offline (no credentials, no network).
|
|
110
117
|
|
|
111
118
|
## Commands
|
|
@@ -117,45 +124,65 @@ n8n-decanter # interactive picker (terminal, inited
|
|
|
117
124
|
# then run verbs (unpulled: Enter pulls);
|
|
118
125
|
# stays in the workflow's menu between
|
|
119
126
|
# verbs, Esc backs out, Esc Esc quits
|
|
127
|
+
# Setup
|
|
120
128
|
n8n-decanter init [dir] # interactive bootstrap (see Setup)
|
|
121
|
-
n8n-decanter
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
n8n-decanter [
|
|
126
|
-
n8n-decanter
|
|
127
|
-
n8n-decanter <ref> rename --workflow "<new name>" # rename the workflow
|
|
128
|
-
n8n-decanter <ref> add "<Node name>" [--ts] # scaffold a Code node (offline)
|
|
129
|
-
n8n-decanter [ref] watch # push a workflow's nodes on save
|
|
129
|
+
n8n-decanter completion zsh|bash # print a shell completion script
|
|
130
|
+
|
|
131
|
+
# Sync
|
|
132
|
+
n8n-decanter pull [workflow…] # remote -> workflows/<kebab>/
|
|
133
|
+
n8n-decanter push [workflow…] [--force] [--no-typecheck]
|
|
134
|
+
n8n-decanter watch [workflow] # push a workflow's nodes on save
|
|
130
135
|
# (+ browser live-reload, opt-in)
|
|
131
|
-
n8n-decanter [
|
|
132
|
-
n8n-decanter [
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
n8n-decanter <
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
n8n-decanter
|
|
136
|
+
n8n-decanter publish [workflow…] # take the draft(s) live
|
|
137
|
+
n8n-decanter unpublish [workflow…] # (unpublish returns to draft-only)
|
|
138
|
+
|
|
139
|
+
# Workflow lifecycle
|
|
140
|
+
n8n-decanter create "<name>" # create a blank workflow, then pull it
|
|
141
|
+
n8n-decanter duplicate <workflow> ["<name>"] # clone a workflow, then pull it
|
|
142
|
+
n8n-decanter delete <workflow> [--force] # delete from the server (folder kept)
|
|
143
|
+
n8n-decanter rename <workflow> "<new name>" # rename the workflow (offline; folder stays)
|
|
144
|
+
|
|
145
|
+
# Inspect & test
|
|
146
|
+
n8n-decanter status [workflow…] [--diff] # drift report (--diff: line diffs);
|
|
147
|
+
# exits 1 on conflict/remote drift
|
|
148
|
+
n8n-decanter check [workflow…] # offline layout-compliance + typecheck
|
|
149
|
+
n8n-decanter executions [workflow…] [--status=success|error|waiting] [--limit=N]
|
|
139
150
|
# fetch recent execution data (run JSON)
|
|
140
|
-
# into workflows/<
|
|
141
|
-
# (gitignored
|
|
142
|
-
|
|
143
|
-
n8n-decanter [
|
|
144
|
-
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
n8n-decanter
|
|
151
|
+
# into workflows/<folder>/executions/
|
|
152
|
+
# (gitignored; numeric arg = one by id)
|
|
153
|
+
n8n-decanter executions [workflow…] clean # delete fetched execution data (offline)
|
|
154
|
+
n8n-decanter data-tables [table…] [--filter='<json>'] [--search=…] [--sort=col:asc|desc] [--limit=N] [--all]
|
|
155
|
+
# fetch data-table schema + rows (read-only)
|
|
156
|
+
# into a gitignored top-level data-tables/;
|
|
157
|
+
# filter/search/sort pull a slice server-side
|
|
158
|
+
n8n-decanter data-tables [table…] clean # delete fetched data-table data (offline)
|
|
159
|
+
n8n-decanter simulate <workflow> [--execution <execution-id>] [--network-none] [--json]
|
|
160
|
+
# replay the whole workflow through a real
|
|
161
|
+
# n8n engine (Docker): pure nodes run for
|
|
162
|
+
# real, network nodes pinned from a capture,
|
|
163
|
+
# credentials stripped; diffs each node vs
|
|
164
|
+
# the capture, exits 1 on divergence. In a
|
|
165
|
+
# terminal, prints a URL to open the run in
|
|
166
|
+
# n8n. Defaults to the newest capture.
|
|
167
|
+
n8n-decanter simulate <workflow> --pin <execution-id> # save a capture's outputs as fixtures/
|
|
168
|
+
n8n-decanter list [--remote] [--json] # pulled workflows: name, id, folder
|
|
169
|
+
# (--remote adds unpulled ones; --json for tooling)
|
|
170
|
+
|
|
171
|
+
# Node
|
|
172
|
+
n8n-decanter node create <workflow> "<Node name>" [--ts] # scaffold a Code node (offline)
|
|
173
|
+
n8n-decanter node rename <workflow> "<old node>" "<new node>" # rename a node everywhere (offline)
|
|
174
|
+
n8n-decanter node run <node-file> [fixture.json] [--allow-env] # run a node offline, print items
|
|
148
175
|
```
|
|
149
176
|
|
|
150
|
-
A workflow
|
|
151
|
-
prefix** — `n8n-decanter "Order Sync"
|
|
177
|
+
A `<workflow>` is its **id, its workflow/folder name, or a unique name
|
|
178
|
+
prefix** — `n8n-decanter push "Order Sync"` and `n8n-decanter push order` both
|
|
152
179
|
work. Matching is case-insensitive and never prompts: an ambiguous or unknown
|
|
153
180
|
name errors with the candidate list. `pull` resolves not-yet-pulled names
|
|
154
|
-
against the server's workflow list
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
may appear in any position
|
|
181
|
+
against the server's workflow list. Without a workflow argument, all workflows
|
|
182
|
+
from the config are processed (or, on a terminal, the picker opens). **The verb
|
|
183
|
+
comes first** (`n8n-decanter push wf123`); everything after it is an argument,
|
|
184
|
+
so a workflow named like a verb needs no special rule and verb-last errors with
|
|
185
|
+
*unknown verb*. Flags may still appear in any position.
|
|
159
186
|
|
|
160
187
|
Output is styled (color, `✓`/`!`/`✗` glyphs, progress) **only when writing to
|
|
161
188
|
a terminal** and respects `NO_COLOR`/`FORCE_COLOR`; piped or redirected output
|
|
@@ -171,25 +198,32 @@ exit 0 — scripts and CI can gate on it like on `check`. `status --diff`
|
|
|
171
198
|
shows the actual line diff under each drifted node, so you see what a push
|
|
172
199
|
would overwrite before running it. API requests time out after 30 s (set
|
|
173
200
|
`"requestTimeoutMs"` in `decanter.config.json` for slow instances), and
|
|
174
|
-
`DEBUG=1` prints full stack traces on errors. `run` fakes the full Code-node
|
|
201
|
+
`DEBUG=1` prints full stack traces on errors. `node run` fakes the full Code-node
|
|
175
202
|
context including `$getWorkflowStaticData`, seeded from `workflow.json` and
|
|
176
|
-
overridable per fixture. Like n8n's own scoped `$env`, `run`'s `$env` is
|
|
203
|
+
overridable per fixture. Like n8n's own scoped `$env`, `node run`'s `$env` is
|
|
177
204
|
**empty by default** — set it explicitly with the fixture's `"env"` field, or
|
|
178
205
|
pass `--allow-env` to inherit the CLI process's environment (which may include
|
|
179
206
|
`N8N_API_KEY` and other secrets), so a node that prints `$env` never leaks the
|
|
180
207
|
host environment by accident.
|
|
181
208
|
|
|
182
209
|
`executions` is read-only against the API and writes each execution as
|
|
183
|
-
`workflows/<
|
|
184
|
-
node produced), meant as temporary reference for building `run` fixtures.
|
|
210
|
+
`workflows/<folder>/executions/<execution-id>.json` — real run data (the items every
|
|
211
|
+
node produced), meant as temporary reference for building `node run` fixtures.
|
|
185
212
|
The dir is written self-gitignored (run data can contain credentials/PII and
|
|
186
213
|
must never land in git); executions reflect the *published* workflow version
|
|
187
214
|
(n8n 2.x), so treat them as convenience data, not ground truth, and remove
|
|
188
215
|
them with `executions clean` when done.
|
|
189
216
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
217
|
+
`data-tables` is the data-table analogue: also read-only against the API, it
|
|
218
|
+
fetches each n8n data table's schema (`columns.json`) and rows (`rows.json`)
|
|
219
|
+
into a **top-level, gitignored** `data-tables/<table>/` dir (data tables are
|
|
220
|
+
project-scoped, not owned by a workflow) so you can develop and debug against
|
|
221
|
+
real table contents. `--filter='<json>'`, `--search`, and `--sort` pull only a
|
|
222
|
+
slice of a large table server-side (the applied filter is recorded in each
|
|
223
|
+
table's `meta.json`); `--limit`/`--all` control page size and exhaustion. It
|
|
224
|
+
never writes a data table. It's gated by the `"dataTables"` config key
|
|
225
|
+
(default on); `data-tables clean` removes the dir (offline). Data tables need
|
|
226
|
+
n8n ≥ 2.x.
|
|
193
227
|
|
|
194
228
|
## How it compares
|
|
195
229
|
|
|
@@ -199,6 +233,16 @@ how that focus stacks up against the native editor and against
|
|
|
199
233
|
[n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) — a broader,
|
|
200
234
|
whole-workflow authoring toolkit.
|
|
201
235
|
|
|
236
|
+
> **Choose [n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) if you…**
|
|
237
|
+
> need an enterprise-ready automation framework — where AI agents assemble
|
|
238
|
+
> complete workflows, multi-environment Dev → Prod pipelines automatically handle
|
|
239
|
+
> credential governance, and full TypeScript GitOps ensures strict auditability
|
|
240
|
+
> across your team.
|
|
241
|
+
>
|
|
242
|
+
> **Choose n8n-decanter if you…** focus on developer experience inside the Code
|
|
243
|
+
> node — enabling typed TypeScript, shared utility libraries, and live
|
|
244
|
+
> hot-reloading DX directly between VS Code and your n8n canvas (even on Cloud).
|
|
245
|
+
|
|
202
246
|
| Capability | Native n8n (browser) | n8n-as-code | n8n-decanter |
|
|
203
247
|
|---|---|---|---|
|
|
204
248
|
| **TypeScript for Code nodes** | ❌ JavaScript or Python only | ❌ TS is at workflow level, not node logic | ✅ Code nodes as `.ts`, compiled on push, typed n8n globals |
|
|
@@ -206,10 +250,10 @@ whole-workflow authoring toolkit.
|
|
|
206
250
|
| **Code as individual files** | ❌ no source files (JSON blob) | 🟡 one `.workflow.ts` per workflow | ✅ folder per workflow; each Code node its own `.js`/`.ts` |
|
|
207
251
|
| **Versioning** | 🟡 in-app history (DB snapshots, tiered retention); Git source control is Enterprise-only | ✅ GitOps sync of workflow source | ✅ real git — diffs, PRs, blame; auto-commit each push/pull |
|
|
208
252
|
| **Live editing** | ✅ the canvas (baseline) | 🟡 explicit pull/push, no auto-watch | ✅ `watch`: push on save + auto-reload the editor tab |
|
|
209
|
-
| **Offline testing on historic executions** | 🟡 re-run past executions / pin data, but online in-editor | 🟡 inspect executions against a live env | ✅ fetch real run JSON → offline `run` fixtures, no creds/network |
|
|
210
|
-
| **Agent-native tooling** | 🟡 n8n's own canvas AI, not your agent on the codebase | ✅ Agent Workbench, skills, MCP, Claude/editor plugins | ✅ scaffolds Claude Code / Cursor / Codex configs + MCP; offline `check`/`run` loop; guardrails |
|
|
253
|
+
| **Offline testing on historic executions** | 🟡 re-run past executions / pin data, but online in-editor | 🟡 inspect executions against a live env | ✅ fetch real run JSON → offline `node run` fixtures, no creds/network |
|
|
254
|
+
| **Agent-native tooling** | 🟡 n8n's own canvas AI, not your agent on the codebase | ✅ Agent Workbench, skills, MCP, Claude/editor plugins | ✅ scaffolds Claude Code / Cursor / Codex configs + MCP; offline `check`/`node run` loop; guardrails |
|
|
211
255
|
| **Model ownership** | ❌ locked to n8n's own hosted AI; can't use your Claude subscription | 🟡 beta Claude Code plugin uses your subscription; flagship Workbench needs an Anthropic key for Claude | ✅ never calls an LLM itself — your agent/subscription does 100%, no key or model config ever |
|
|
212
|
-
| **Agentic workflow creation** | 🟡 AI Workflow Builder (natural language), but Cloud / plan-gated — credits, self-host needs setup | ✅ 537 node schemas + 7,700+ templates + skills | 🟡 today via scaffolded n8n-mcp; first-party repo-authored creation planned (`
|
|
256
|
+
| **Agentic workflow creation** | 🟡 AI Workflow Builder (natural language), but Cloud / plan-gated — credits, self-host needs setup | ✅ 537 node schemas + 7,700+ templates + skills | 🟡 today via scaffolded n8n-mcp; first-party repo-authored creation planned (`node create` + `push --create`) |
|
|
213
257
|
| **Whole-workflow TypeScript authoring** | ❌ | ✅ `.workflow.ts` decorator classes (structure + links) | ❌ keeps `workflow.json`; extracts Code-node source only |
|
|
214
258
|
| **Multi-environment promotion** | 🟡 Enterprise source control / environments | ✅ `promote` remaps creds + refs Dev→Prod | 🟡 separate sync dir per instance, but no `promote` (IDs/creds/refs not remapped) |
|
|
215
259
|
|
|
@@ -221,7 +265,6 @@ n8n-as-code shines for whole-workflow authoring/generation and multi-environment
|
|
|
221
265
|
ops; the native editor stays the live visual canvas everything syncs back to. And
|
|
222
266
|
decanter makes no LLM calls of its own — you drive it with the coding agent you
|
|
223
267
|
already run, so Claude Code on a Claude subscription needs no extra API tokens.
|
|
224
|
-
-->
|
|
225
268
|
|
|
226
269
|
## Caveats
|
|
227
270
|
|
package/dist/lib/api.mjs
CHANGED
|
@@ -45,6 +45,49 @@ export class N8nApi {
|
|
|
45
45
|
async getExecution(id) {
|
|
46
46
|
return this.#request("GET", `/api/v1/executions/${encodeURIComponent(id)}?includeData=true`);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* All data tables (n8n ≥ 2.x built-in project tables), cursor-paginated like
|
|
50
|
+
* `listWorkflows`. Read-only by design — the data-table API is never written
|
|
51
|
+
* through (see lib/datatables.mts). On pre-2.x instances the endpoint 404s;
|
|
52
|
+
* the caller surfaces that as a friendly hint.
|
|
53
|
+
*/
|
|
54
|
+
async listDataTables() {
|
|
55
|
+
const all = [];
|
|
56
|
+
let cursor;
|
|
57
|
+
do {
|
|
58
|
+
const query = new URLSearchParams({ limit: "100", ...(cursor !== undefined && { cursor }) });
|
|
59
|
+
const page = (await this.#request("GET", `/api/v1/data-tables?${query}`));
|
|
60
|
+
all.push(...page.data);
|
|
61
|
+
cursor = page.nextCursor ?? undefined;
|
|
62
|
+
} while (cursor !== undefined);
|
|
63
|
+
return all;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A data table's columns (its schema). Tolerates both a bare array and a
|
|
67
|
+
* `{ data: [...] }` envelope so a shape difference between n8n versions
|
|
68
|
+
* doesn't break the fetch. Read-only.
|
|
69
|
+
*/
|
|
70
|
+
async getDataTableColumns(id) {
|
|
71
|
+
const res = (await this.#request("GET", `/api/v1/data-tables/${encodeURIComponent(id)}/columns`));
|
|
72
|
+
return Array.isArray(res) ? res : res.data;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* One page of a data table's rows. The server-side `filter` (a JSON string of
|
|
76
|
+
* conditions), `search` (free text over string columns), and `sortBy`
|
|
77
|
+
* (`col:asc|desc`) narrow the result so callers pull only the rows they need
|
|
78
|
+
* from a potentially huge table; `limit` (default 100, API cap 250) + `cursor`
|
|
79
|
+
* paginate. Returns the page plus `nextCursor`. Read-only.
|
|
80
|
+
*/
|
|
81
|
+
async getDataTableRows(id, { limit = 100, cursor, filter, search, sortBy } = {}) {
|
|
82
|
+
const query = new URLSearchParams({
|
|
83
|
+
limit: String(limit),
|
|
84
|
+
...(cursor !== undefined && { cursor }),
|
|
85
|
+
...(filter !== undefined && { filter }),
|
|
86
|
+
...(search !== undefined && { search }),
|
|
87
|
+
...(sortBy !== undefined && { sortBy }),
|
|
88
|
+
});
|
|
89
|
+
return (await this.#request("GET", `/api/v1/data-tables/${encodeURIComponent(id)}/rows?${query}`));
|
|
90
|
+
}
|
|
48
91
|
async updateWorkflow(id, body) {
|
|
49
92
|
return this.#request("PUT", `/api/v1/workflows/${encodeURIComponent(id)}`, body);
|
|
50
93
|
}
|
package/dist/lib/config.mjs
CHANGED
|
@@ -49,6 +49,8 @@ export function loadConfig(cwd = process.cwd(), { requireCredentials = true } =
|
|
|
49
49
|
browserReload: cfg.browserReload === "proxy" ? "proxy" : "off",
|
|
50
50
|
proxyPort: typeof cfg.proxyPort === "number" ? cfg.proxyPort : 5679,
|
|
51
51
|
requestTimeoutMs: typeof cfg.requestTimeoutMs === "number" && cfg.requestTimeoutMs > 0 ? cfg.requestTimeoutMs : 30_000,
|
|
52
|
+
n8nVersion: typeof cfg.n8nVersion === "string" && cfg.n8nVersion !== "" ? cfg.n8nVersion : undefined,
|
|
53
|
+
dataTables: cfg.dataTables !== false,
|
|
52
54
|
host,
|
|
53
55
|
apiKey,
|
|
54
56
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { kebabCase } from "./util.mjs";
|
|
4
|
+
/** Top-level dir (next to decanter.config.json) holding fetched data tables. */
|
|
5
|
+
export const DATA_TABLES_DIR = "data-tables";
|
|
6
|
+
/** Default rows fetched per page — the API caps a page at 250. */
|
|
7
|
+
export const DEFAULT_ROW_LIMIT = 100;
|
|
8
|
+
/**
|
|
9
|
+
* Folder slug for a fetched table: the kebab of its name with the id appended
|
|
10
|
+
* (data-table names aren't guaranteed unique, so the id disambiguates). The id
|
|
11
|
+
* is stripped of anything non-alphanumeric to stay filesystem-safe.
|
|
12
|
+
*/
|
|
13
|
+
export function dataTableSlug(id, name) {
|
|
14
|
+
const safeId = String(id).replace(/[^A-Za-z0-9]+/g, "") || "id";
|
|
15
|
+
return `${kebabCase(name)}-${safeId}`;
|
|
16
|
+
}
|
|
17
|
+
/** Pick the tables named by `tableRefs` (id or exact name); empty = all. */
|
|
18
|
+
function selectTables(tables, tableRefs) {
|
|
19
|
+
if (tableRefs.length === 0)
|
|
20
|
+
return tables;
|
|
21
|
+
const picked = new Map();
|
|
22
|
+
for (const ref of tableRefs) {
|
|
23
|
+
const lc = ref.toLowerCase();
|
|
24
|
+
const hits = tables.filter((t) => String(t.id) === ref || t.name.toLowerCase() === lc);
|
|
25
|
+
if (hits.length === 0) {
|
|
26
|
+
const known = tables.map((t) => `"${t.name}" (${t.id})`).join(", ");
|
|
27
|
+
throw new Error(`no data table matches "${ref}"${known ? ` — tables: ${known}` : " — no data tables on the server"}`);
|
|
28
|
+
}
|
|
29
|
+
for (const t of hits)
|
|
30
|
+
picked.set(String(t.id), t);
|
|
31
|
+
}
|
|
32
|
+
return [...picked.values()];
|
|
33
|
+
}
|
|
34
|
+
/** Human summary fragment naming the applied server-side filter, or "". */
|
|
35
|
+
function filterSummary(opts) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
if (opts.filter !== undefined)
|
|
38
|
+
parts.push(`filter ${opts.filter}`);
|
|
39
|
+
if (opts.search !== undefined)
|
|
40
|
+
parts.push(`search "${opts.search}"`);
|
|
41
|
+
if (opts.sortBy !== undefined)
|
|
42
|
+
parts.push(`sort ${opts.sortBy}`);
|
|
43
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fetch data-table schemas + (filtered) rows into a single top-level,
|
|
47
|
+
* self-ignored `data-tables/` dir — mirrors `executions` (read-only, temp,
|
|
48
|
+
* gitignored). Data tables are project-scoped (not owned by a workflow), so
|
|
49
|
+
* they land next to `decanter.config.json`, not under a workflow folder. Each
|
|
50
|
+
* table becomes `data-tables/<slug>/{meta,columns,rows}.json`; `meta.json`
|
|
51
|
+
* records the applied filter/search/sort/limit and the row count so a filtered
|
|
52
|
+
* `rows.json` is self-describing. The CLI never writes any data-table endpoint.
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchDataTables(api, configDir, opts, log) {
|
|
55
|
+
let tables;
|
|
56
|
+
try {
|
|
57
|
+
tables = await api.listDataTables();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err.message;
|
|
61
|
+
if (/\b404\b/.test(msg)) {
|
|
62
|
+
throw new Error(`the n8n data-tables API is not available (404) — data tables need n8n ≥ 2.x; a full-access key still works if your instance predates them\n${msg}`);
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
const selected = selectTables(tables, opts.tableRefs ?? []);
|
|
67
|
+
if (selected.length === 0) {
|
|
68
|
+
log.info("no data tables on the server");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const outRoot = path.join(configDir, DATA_TABLES_DIR);
|
|
72
|
+
mkdirSync(outRoot, { recursive: true });
|
|
73
|
+
// Self-ignoring dir: data tables can hold PII — same reasoning as executions/.
|
|
74
|
+
writeFileSync(path.join(outRoot, ".gitignore"), "*\n");
|
|
75
|
+
const rel = (p) => path.relative(process.cwd(), p);
|
|
76
|
+
const filterDesc = filterSummary(opts);
|
|
77
|
+
for (const table of selected) {
|
|
78
|
+
const dir = path.join(outRoot, dataTableSlug(table.id, table.name));
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
const columns = await api.getDataTableColumns(String(table.id));
|
|
81
|
+
const rows = [];
|
|
82
|
+
let cursor;
|
|
83
|
+
do {
|
|
84
|
+
const page = await api.getDataTableRows(String(table.id), {
|
|
85
|
+
limit: opts.limit ?? DEFAULT_ROW_LIMIT,
|
|
86
|
+
cursor,
|
|
87
|
+
filter: opts.filter,
|
|
88
|
+
search: opts.search,
|
|
89
|
+
sortBy: opts.sortBy,
|
|
90
|
+
});
|
|
91
|
+
rows.push(...page.data);
|
|
92
|
+
cursor = opts.all ? (page.nextCursor ?? undefined) : undefined;
|
|
93
|
+
} while (cursor !== undefined);
|
|
94
|
+
const meta = {
|
|
95
|
+
id: table.id,
|
|
96
|
+
name: table.name,
|
|
97
|
+
projectId: table.projectId ?? null,
|
|
98
|
+
fetchedAt: new Date().toISOString(),
|
|
99
|
+
rowCount: rows.length,
|
|
100
|
+
// A filtered rows.json is a slice, never the whole table — record what
|
|
101
|
+
// produced it so it can't be mistaken for a full export.
|
|
102
|
+
filter: opts.filter ?? null,
|
|
103
|
+
search: opts.search ?? null,
|
|
104
|
+
sortBy: opts.sortBy ?? null,
|
|
105
|
+
limit: opts.limit ?? DEFAULT_ROW_LIMIT,
|
|
106
|
+
all: Boolean(opts.all),
|
|
107
|
+
};
|
|
108
|
+
writeFileSync(path.join(dir, "meta.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
109
|
+
writeFileSync(path.join(dir, "columns.json"), JSON.stringify(columns, null, 2) + "\n");
|
|
110
|
+
writeFileSync(path.join(dir, "rows.json"), JSON.stringify(rows, null, 2) + "\n");
|
|
111
|
+
log.info(`wrote ${rel(dir)}/{meta,columns,rows}.json (${columns.length} column${columns.length === 1 ? "" : "s"}, ${rows.length} row${rows.length === 1 ? "" : "s"})`);
|
|
112
|
+
}
|
|
113
|
+
log.ok(`${selected.length} data table${selected.length === 1 ? "" : "s"}${filterDesc} -> ${rel(outRoot)} (gitignored — temp data, "data-tables clean" removes it)`);
|
|
114
|
+
}
|
|
115
|
+
/** Offline delete of the local `data-tables/` dir (mirrors cleanExecutions). */
|
|
116
|
+
export function cleanDataTables(configDir, log) {
|
|
117
|
+
const outRoot = path.join(configDir, DATA_TABLES_DIR);
|
|
118
|
+
if (!existsSync(outRoot)) {
|
|
119
|
+
log.info("no data-tables/ dir to clean");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
rmSync(outRoot, { recursive: true, force: true });
|
|
123
|
+
log.ok(`removed ${path.relative(process.cwd(), outRoot)}`);
|
|
124
|
+
}
|