n8n-decanter 0.3.2 → 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 CHANGED
@@ -7,6 +7,200 @@ 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
+
134
+ ## [0.4.1] - 2026-07-20
135
+
136
+ ### Changed
137
+
138
+ - **Refreshed the scaffolded agent guide (`AGENTS.md`).** It now steers agents
139
+ to the `rename` and `duplicate` verbs (rename led with the command instead of
140
+ a hand-edit checklist, `duplicate` added to the new-workflow and command
141
+ taxonomies), opens with a compact "short version" of the hard invariants,
142
+ points at `n8n-globals.d.ts` as the authoritative globals list instead of an
143
+ inline copy that could drift, and drops a stale reference to a non-existent
144
+ `SCAFFOLD.md`.
145
+
146
+ ## [0.4.0] - 2026-07-20
147
+
148
+ ### Added
149
+
150
+ - **`add` verb** — `n8n-decanter <ref> add "<Node name>" [--ts]` scaffolds a
151
+ Code node into a pulled workflow in one offline step: it mints the node id,
152
+ writes the `code/` source file (kebab-case, with the `-<id8>` collision
153
+ suffix), adds the node object plus its `//@file:` placeholder, and registers
154
+ it in `.decanter.json`, then re-checks the folder. The node lands
155
+ **disconnected** (wire it in the editor); `--ts` scaffolds a `.ts` source.
156
+ The next `push` propagates it.
157
+ - **`duplicate` verb** — `n8n-decanter <ref> duplicate ["<new name>"]` clones an
158
+ already-pulled workflow into a **new workflow on the server** and pulls the
159
+ copy. The clone carries the repo's current content (placeholders
160
+ reconstituted from `code/`, `.ts` nodes compiled), is born **unpublished**,
161
+ and defaults its name to `"<name> (copy)"`. The source folder and the source
162
+ remote workflow are left untouched.
163
+
164
+ ### Removed
165
+
166
+ - **Breaking: the `uuid` verb is gone.** Its only job was minting a node id for
167
+ hand-adding a Code node — now `add` does the whole scaffold (id included) in
168
+ one guard-checked step, so a bare id generator is redundant. Use
169
+ `n8n-decanter <ref> add "<Node name>"` instead.
170
+
171
+ ## [0.3.4] - 2026-07-20
172
+
173
+ ### Added
174
+
175
+ - **Modification-aware template refresh.** `init` now records a copy-time
176
+ baseline of every template file in a git-tracked `.decanter-template.json`
177
+ manifest. Re-running `init` uses it to refresh files you haven't touched
178
+ (after a confirm), pull in files newly added to the template, and **leave
179
+ your local edits alone** — reporting them as drift instead of silently
180
+ keeping the old version. Files that changed in both the template and your
181
+ copy are flagged as conflicts and left untouched.
182
+
183
+ ### Changed
184
+
185
+ - **Re-running `init` is no longer all-or-nothing.** Previously the default
186
+ refused to overwrite anything and `--force` clobbered every template file.
187
+ Now the default is modification-aware (see above); `--force` is unchanged —
188
+ the escape hatch that overwrites everything, now noting which files "had
189
+ local changes" as it goes.
190
+
191
+ ## [0.3.3] - 2026-07-20
192
+
193
+ ### Changed
194
+
195
+ - **Interactive picker got a visual refresh.** Each workflow row now leads with
196
+ a `●` (pulled) / `○` (not pulled) status glyph and the ids line up in an
197
+ aligned column; each stage carries a short title (`pick a workflow` over the
198
+ list, the workflow name over its verb menu). The state distinction is now
199
+ carried by the glyph *shape*, so the per-row `(not pulled)` words are gone —
200
+ the key is stated once in a footer legend (`● pulled · ○ not pulled`), and
201
+ the output stays legible under `NO_COLOR`. Behavior (filtering, navigation,
202
+ verbs) is unchanged.
203
+
10
204
  ## [0.3.2] - 2026-07-20
11
205
 
12
206
  ### Fixed
package/README.md CHANGED
@@ -4,12 +4,17 @@
4
4
  [![npm](https://img.shields.io/npm/v/n8n-decanter)](https://www.npmjs.com/package/n8n-decanter)
5
5
  [![Docs](https://img.shields.io/badge/docs-website-blue.svg)](https://buttjer.github.io/n8n-decanter/)
6
6
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![vibe coded](https://img.shields.io/badge/vibe%20coded-with%20Claude%20Code-8A2BE2)](https://claude.com/claude-code)
7
8
 
8
9
  **Work on n8n like a codebase — built for AI coding agents.**
9
10
 
10
11
  **Pre-1.0 — breaking changes to the data model or CLI may ship in minor
11
12
  versions until v1.0.**
12
13
 
14
+ > **Built with AI agents.** Much of this codebase was written by Claude Code
15
+ > under human review. It's tested (CI + a real-n8n integration suite) and used
16
+ > in earnest, but treat pre-1.0 the way the version implies.
17
+
13
18
  n8n-decanter syncs your n8n instance into a git-friendly, folder-per-workflow
14
19
  layout: every Code node's source becomes its own `.js` or `.ts` file,
15
20
  editable in your IDE or by your agent, and pushed back through the n8n API.
@@ -21,14 +26,19 @@ editable in your IDE or by your agent, and pushed back through the n8n API.
21
26
  - **TypeScript or typed JS** — write nodes in TS (compiled on push); n8n
22
27
  globals (`$input`, `$('…')`, …) are typed in both.
23
28
  - **Agent-native** — `init` scaffolds Claude Code / Cursor / Codex configs
24
- and verification hooks; offline `check` and `run` give agents a
29
+ and verification hooks; offline `check` and `node run` give agents a
25
30
  credential-free feedback loop.
26
31
  - **Guardrails** — a compliance guard and typecheck gate block broken
27
32
  pushes; a drift guard keeps you from clobbering remote edits.
28
33
  - **Real execution data on tap** — `executions` fetches recent run JSON into
29
34
  a gitignored temp dir, so agents see actual payload shapes (and build
30
- accurate `run` fixtures) instead of guessing; `executions clean` removes
35
+ accurate `node run` fixtures) instead of guessing; `executions clean` removes
31
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.
32
42
  - **Live editing** — `watch` pushes on save and auto-reloads the n8n editor
33
43
  tab via a local proxy.
34
44
  - **Shared code and small libraries** — `.ts` nodes import helpers/types
@@ -64,9 +74,11 @@ directly, no build needed.
64
74
 
65
75
  `init` copies everything in [template/](template/); files named `X.example`
66
76
  land as `X` (the suffix keeps agent configs inert in this repo, live in the
67
- target). It never overwrites existing files (safe to re-run) unless you pass
68
- `--force`, which re-copies template files over existing ones (`.env` is never
69
- touched by it). When `.env` already holds both values, init skips the
77
+ target). Re-running `init` is safe and **modification-aware**: it refreshes
78
+ template files you haven't edited (after a confirm), leaves your local changes
79
+ alone (reporting them as drift), and picks up files new to the template —
80
+ tracked via a `.decanter-template.json` baseline. `--force` is the escape hatch
81
+ that overwrites everything (`.env` is never touched). When `.env` already holds both values, init skips the
70
82
  prompts and reuses them — edit or delete `.env` to change credentials. It
71
83
  also does a best-effort credential check. Alternatively set up manually: `cp .env.example .env` and
72
84
  fill it in. Then add workflow ids to `decanter.config.json`:
@@ -83,6 +95,8 @@ so a leaked `.env` has a small blast radius:
83
95
  - `workflow:create`, `workflow:delete` — `create` / `delete`
84
96
  - `workflow:activate`, `workflow:deactivate` — `publish` / `unpublish`
85
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)
86
100
 
87
101
  A full-access key also works if your n8n version predates scoped keys.
88
102
 
@@ -98,7 +112,7 @@ PostToolUse hook that runs `check` after node edits. Verification routes through
98
112
  the CLI, so `n8n-decanter` must be on the sync dir's PATH: install it globally
99
113
  (`npm i -g n8n-decanter`), add it to the sync dir's `devDependencies`, or
100
114
  `npm link` a git checkout (build it first — Node won't type-strip `.mts`
101
- under `node_modules`). The verbs `check`, `run`, and `uuid` are fully
115
+ under `node_modules`). The verbs `check`, `node run`, `rename`, and `node create` are fully
102
116
  offline (no credentials, no network).
103
117
 
104
118
  ## Commands
@@ -110,44 +124,65 @@ n8n-decanter # interactive picker (terminal, inited
110
124
  # then run verbs (unpulled: Enter pulls);
111
125
  # stays in the workflow's menu between
112
126
  # verbs, Esc backs out, Esc Esc quits
127
+ # Setup
113
128
  n8n-decanter init [dir] # interactive bootstrap (see Setup)
114
- n8n-decanter [ref...] pull # remote -> workflows/<Name>/
115
- n8n-decanter [ref...] push [--force] [--no-typecheck]
116
- n8n-decanter [ref...] status [--diff] # drift report (--diff: line diffs);
117
- # exits 1 on conflict/remote drift
118
- n8n-decanter [ref...] check # offline layout-compliance + typecheck
119
- n8n-decanter <ref> rename "<old node>" "<new node>" # rename a node everywhere
120
- n8n-decanter <ref> rename --workflow "<new name>" # rename the workflow
121
- 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
122
135
  # (+ browser live-reload, opt-in)
123
- n8n-decanter [ref...] publish # take the draft(s) live
124
- n8n-decanter [ref...] unpublish # (unpublish returns to draft-only)
125
- n8n-decanter create "<name>" # create a blank workflow, then pull it
126
- n8n-decanter <ref> delete [--force] # delete a workflow from the server
127
- # (y/N confirm; --force skips it; the
128
- # local folder is left untouched)
129
- n8n-decanter [ref...] executions [--status=success|error|waiting] [--limit=N]
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]
130
150
  # fetch recent execution data (run JSON)
131
- # into workflows/<Name>/executions/
132
- # (gitignored temp files; a numeric arg
133
- # fetches that one execution by id)
134
- n8n-decanter [ref...] executions clean # delete fetched execution data (offline)
135
- n8n-decanter list [--remote] # pulled workflows: name, id, folder
136
- # (--remote adds unpulled ones)
137
- n8n-decanter completion zsh|bash # print a shell completion script
138
- n8n-decanter <node-file> run [fixture.json] [--allow-env] # run a node offline, print items
139
- n8n-decanter uuid [count] # lowercase v4 UUID(s) for new node ids
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
140
175
  ```
141
176
 
142
- A workflow `<ref>` is its **id, its workflow/folder name, or a unique name
143
- prefix** — `n8n-decanter "Order Sync" push` and `n8n-decanter order push` both
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
144
179
  work. Matching is case-insensitive and never prompts: an ambiguous or unknown
145
180
  name errors with the candidate list. `pull` resolves not-yet-pulled names
146
- against the server's workflow list; a workflow literally named like a verb
147
- must be addressed by id. Without refs, all workflows from the config are
148
- processed. The verb may sit anywhere among the arguments (`push wf123` ==
149
- `wf123 push`) — the first token matching a known verb is the command; flags
150
- may appear in any position too.
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.
151
186
 
152
187
  Output is styled (color, `✓`/`!`/`✗` glyphs, progress) **only when writing to
153
188
  a terminal** and respects `NO_COLOR`/`FORCE_COLOR`; piped or redirected output
@@ -163,25 +198,32 @@ exit 0 — scripts and CI can gate on it like on `check`. `status --diff`
163
198
  shows the actual line diff under each drifted node, so you see what a push
164
199
  would overwrite before running it. API requests time out after 30 s (set
165
200
  `"requestTimeoutMs"` in `decanter.config.json` for slow instances), and
166
- `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
167
202
  context including `$getWorkflowStaticData`, seeded from `workflow.json` and
168
- 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
169
204
  **empty by default** — set it explicitly with the fixture's `"env"` field, or
170
205
  pass `--allow-env` to inherit the CLI process's environment (which may include
171
206
  `N8N_API_KEY` and other secrets), so a node that prints `$env` never leaks the
172
207
  host environment by accident.
173
208
 
174
209
  `executions` is read-only against the API and writes each execution as
175
- `workflows/<Name>/executions/<execId>.json` — real run data (the items every
176
- 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.
177
212
  The dir is written self-gitignored (run data can contain credentials/PII and
178
213
  must never land in git); executions reflect the *published* workflow version
179
214
  (n8n 2.x), so treat them as convenience data, not ground truth, and remove
180
215
  them with `executions clean` when done.
181
216
 
182
- <!--
183
- "How it compares" is temporarily hidden pending the n8n-as-code maintainer's
184
- sign-off on the comparison scorings (asked 2026-07-20). Uncomment to restore.
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.
185
227
 
186
228
  ## How it compares
187
229
 
@@ -191,6 +233,16 @@ how that focus stacks up against the native editor and against
191
233
  [n8n-as-code](https://github.com/EtienneLescot/n8n-as-code) — a broader,
192
234
  whole-workflow authoring toolkit.
193
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
+
194
246
  | Capability | Native n8n (browser) | n8n-as-code | n8n-decanter |
195
247
  |---|---|---|---|
196
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 |
@@ -198,10 +250,10 @@ whole-workflow authoring toolkit.
198
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` |
199
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 |
200
252
  | **Live editing** | ✅ the canvas (baseline) | 🟡 explicit pull/push, no auto-watch | ✅ `watch`: push on save + auto-reload the editor tab |
201
- | **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 |
202
- | **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 |
203
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 |
204
- | **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 (`add` + `push --create`) |
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`) |
205
257
  | **Whole-workflow TypeScript authoring** | ❌ | ✅ `.workflow.ts` decorator classes (structure + links) | ❌ keeps `workflow.json`; extracts Code-node source only |
206
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) |
207
259
 
@@ -213,7 +265,6 @@ n8n-as-code shines for whole-workflow authoring/generation and multi-environment
213
265
  ops; the native editor stays the live visual canvas everything syncs back to. And
214
266
  decanter makes no LLM calls of its own — you drive it with the coding agent you
215
267
  already run, so Claude Code on a Claude subscription needs no extra API tokens.
216
- -->
217
268
 
218
269
  ## Caveats
219
270
 
@@ -0,0 +1,91 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { findWorkflowDir, readState, writeState } from "./state.mjs";
5
+ import { CODE_DIR, CODE_NODE_TYPE, FILE_PLACEHOLDER_PREFIX, kebabCase, stableWorkflowJson } from "./util.mjs";
6
+ import { validateWorkflowDir } from "./validate.mjs";
7
+ /**
8
+ * Starter source for a freshly scaffolded Code node: valid, self-contained
9
+ * (no `$('…')` references, so it lands compliant), and it typechecks under the
10
+ * n8n ambient globals — the same body for `.js` and `.ts`.
11
+ */
12
+ const DEFAULT_SOURCE = `// New Code node — runs once for all items. Edit me, then push.
13
+ // See https://docs.n8n.io/code/ for the Code node API.
14
+ for (const item of $input.all()) {
15
+ item.json.myNewField = 1;
16
+ }
17
+
18
+ return $input.all();
19
+ `;
20
+ /** Rightmost node's x + a step, so a scaffolded node doesn't stack on another. */
21
+ function nextPosition(nodes) {
22
+ let maxX = 0;
23
+ for (const node of nodes) {
24
+ const pos = node.position;
25
+ if (Array.isArray(pos) && typeof pos[0] === "number")
26
+ maxX = Math.max(maxX, pos[0]);
27
+ }
28
+ return [maxX + 220, 0];
29
+ }
30
+ /**
31
+ * Scaffold a `n8n-nodes-base.code` node into a pulled workflow in one atomic,
32
+ * guard-checked step (offline; `push` propagates). Mints a v4 uuid, writes the
33
+ * kebab-case source file under `code/` (sharing pull/rename's `-<id8>` collision
34
+ * suffix), appends the node object (default parameters, `mode:
35
+ * runOnceForAllItems`, a `//@file:` placeholder) to `workflow.json`, and
36
+ * registers it in `.decanter.json`. **No connections are wired** — the node
37
+ * lands disconnected but compliant; wiring stays in the editor. Re-validates
38
+ * afterwards and fails loudly on any violation, exactly like `rename`.
39
+ */
40
+ export function addCodeNode(root, id, nodeName, { ts = false } = {}, log) {
41
+ nodeName = nodeName.trim();
42
+ if (!nodeName)
43
+ throw new Error("node name must not be empty");
44
+ const dir = findWorkflowDir(root, id, log);
45
+ if (!dir)
46
+ throw new Error(`workflow ${id} not found under ${root} — pull it first`);
47
+ const wfFile = path.join(dir, "workflow.json");
48
+ if (!existsSync(wfFile))
49
+ throw new Error(`missing workflow.json in ${dir} — pull first`);
50
+ let wf;
51
+ try {
52
+ wf = JSON.parse(readFileSync(wfFile, "utf8"));
53
+ }
54
+ catch (err) {
55
+ throw new Error(`${wfFile}: invalid JSON (${err.message})`);
56
+ }
57
+ if (wf.nodes.some((n) => n.name === nodeName))
58
+ throw new Error(`a node named "${nodeName}" already exists in "${wf.name}"`);
59
+ const nodeId = randomUUID();
60
+ const ext = ts ? ".ts" : ".js";
61
+ mkdirSync(path.join(dir, CODE_DIR), { recursive: true });
62
+ let base = kebabCase(nodeName);
63
+ if (existsSync(path.join(dir, CODE_DIR, `${base}${ext}`))) {
64
+ base = `${base}-${nodeId.slice(0, 8)}`;
65
+ if (existsSync(path.join(dir, CODE_DIR, `${base}${ext}`))) {
66
+ throw new Error(`cannot add "${nodeName}": both kebab-case targets exist (${CODE_DIR}/${base}${ext})`);
67
+ }
68
+ }
69
+ const file = `${CODE_DIR}/${base}${ext}`;
70
+ writeFileSync(path.join(dir, file), DEFAULT_SOURCE);
71
+ const node = {
72
+ id: nodeId,
73
+ name: nodeName,
74
+ type: CODE_NODE_TYPE,
75
+ typeVersion: 2,
76
+ position: nextPosition(wf.nodes),
77
+ parameters: { mode: "runOnceForAllItems", jsCode: FILE_PLACEHOLDER_PREFIX + file },
78
+ };
79
+ wf.nodes.push(node);
80
+ writeFileSync(wfFile, stableWorkflowJson(wf));
81
+ const state = readState(dir);
82
+ if (state) {
83
+ state.nodes[nodeId] = { file };
84
+ writeState(dir, state);
85
+ }
86
+ log.info(`added Code node "${nodeName}" (${nodeId}) -> ${file} — disconnected; wire it in the editor, then push to propagate`);
87
+ const { errors } = validateWorkflowDir(dir);
88
+ if (errors.length > 0) {
89
+ throw new Error(`add left the workflow non-compliant — files were written, inspect (git diff) and fix:\n${errors.map((e) => ` ${e}`).join("\n")}`);
90
+ }
91
+ }
package/dist/lib/api.mjs CHANGED
@@ -45,18 +45,62 @@ 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
  }
51
94
  /**
52
- * Create a blank draft on the server (n8n 2.x `POST /workflows`). The minimal
53
- * accepted body is name + empty nodes/connections/settings (verified against
54
- * 2.30.7 — name-only is rejected). Born **unpublished** (`active:false`); the
55
- * caller pulls the returned id so the folder + state land. `createWorkflow` is
56
- * the shared entry point Plan 21's `duplicate` reuses.
95
+ * Create a workflow on the server (n8n 2.x `POST /workflows`). The required
96
+ * fields are name + nodes/connections/settings (verified against 2.30.7 —
97
+ * name-only is rejected), so `create` posts empty collections while
98
+ * `duplicate` posts the cloned nodes/connections. Born **unpublished**
99
+ * (`active:false`); the caller pulls the returned id so the folder + state
100
+ * land. Shared by `create` (blank) and `duplicate` (Plan 21).
57
101
  */
58
- async createWorkflow(name) {
59
- return this.#request("POST", "/api/v1/workflows", { name, nodes: [], connections: {}, settings: {} });
102
+ async createWorkflow(body) {
103
+ return this.#request("POST", "/api/v1/workflows", body);
60
104
  }
61
105
  /** Take the draft live (n8n 2.x publish). Idempotent server-side; returns the workflow. */
62
106
  async activateWorkflow(id) {
@@ -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
  };