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 +194 -0
- package/README.md +99 -48
- package/dist/lib/add.mjs +91 -0
- package/dist/lib/api.mjs +51 -7
- 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 +146 -25
- package/dist/lib/lifecycle.mjs +39 -2
- package/dist/lib/picker.mjs +34 -10
- package/dist/lib/pull.mjs +23 -22
- package/dist/lib/push.mjs +1 -1
- package/dist/lib/rename.mjs +11 -2
- package/dist/lib/simulate.mjs +374 -0
- package/dist/lib/state.mjs +10 -6
- package/dist/lib/template.mjs +45 -0
- package/dist/lib/util.mjs +4 -0
- package/dist/n8n-decanter.mjs +273 -86
- package/package.json +4 -2
- package/template/.claude/settings.local.json.example +16 -3
- package/template/.env.example +2 -0
- package/template/AGENTS.md.example +111 -40
- package/template/CLAUDE.md.example +1 -1
- package/template/n8n-globals.d.ts.example +6 -1
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
|
[](https://www.npmjs.com/package/n8n-decanter)
|
|
5
5
|
[](https://buttjer.github.io/n8n-decanter/)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](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).
|
|
68
|
-
|
|
69
|
-
|
|
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 `
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
n8n-decanter [
|
|
119
|
-
n8n-decanter
|
|
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 [
|
|
124
|
-
n8n-decanter [
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
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]
|
|
130
150
|
# fetch recent execution data (run JSON)
|
|
131
|
-
# into workflows/<
|
|
132
|
-
# (gitignored
|
|
133
|
-
|
|
134
|
-
n8n-decanter [
|
|
135
|
-
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
n8n-decanter
|
|
139
|
-
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
|
|
140
175
|
```
|
|
141
176
|
|
|
142
|
-
A workflow
|
|
143
|
-
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
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
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.
|
|
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/<
|
|
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
|
-
|
|
184
|
-
|
|
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 (`
|
|
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
|
|
package/dist/lib/add.mjs
ADDED
|
@@ -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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* the
|
|
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(
|
|
59
|
-
return this.#request("POST", "/api/v1/workflows",
|
|
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) {
|
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
|
};
|