n8n-decanter 0.6.0 → 0.8.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 +459 -0
- package/README.md +25 -18
- package/dist/lib/backup.mjs +34 -24
- package/dist/lib/compile.mjs +11 -2
- package/dist/lib/diff.mjs +75 -3
- package/dist/lib/engine.mjs +3 -2
- package/dist/lib/errors.mjs +20 -0
- package/dist/lib/executions.mjs +5 -5
- package/dist/lib/init.mjs +183 -43
- package/dist/lib/mcpconnect.mjs +15 -2
- package/dist/lib/mcpserve.mjs +23 -0
- package/dist/lib/picker.mjs +95 -2
- package/dist/lib/preflight.mjs +140 -68
- package/dist/lib/prompt.mjs +3 -3
- package/dist/lib/pull.mjs +16 -7
- package/dist/lib/push.mjs +13 -20
- package/dist/lib/run.mjs +79 -12
- package/dist/lib/simulate.mjs +8 -7
- package/dist/lib/skills.mjs +126 -0
- package/dist/lib/state.mjs +40 -3
- package/dist/lib/status.mjs +17 -87
- package/dist/lib/style.mjs +44 -1
- package/dist/lib/testrun.mjs +11 -11
- package/dist/lib/validate.mjs +89 -15
- package/dist/n8n-decanter.mjs +165 -206
- package/{template/n8n-globals.d.ts.example → n8n-globals.d.ts} +16 -3
- package/package.json +13 -4
- package/template/.claude/hooks/mcp-route-check.mjs.example +1 -1
- package/template/.claude/hooks/verify.mjs.example +26 -8
- package/template/.claude/{settings.local.json.example → settings.json.example} +26 -8
- package/template/.mcp.json.example +2 -2
- package/template/AGENTS.md.example +138 -65
- package/template/CLAUDE.md.example +10 -4
- package/template/decanter-ts-plugin/index.js.example +1 -1
- package/template/opencode.json.example +5 -2
- package/template/package.json.example +2 -2
- package/template/tsconfig.json.example +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,465 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **The interactive picker offers a `--force` retry when a `push` hits the drift
|
|
15
|
+
guard.** Previously the error was printed and you were dropped back at the
|
|
16
|
+
menu, having to leave the picker and re-run `push --force` by hand — even
|
|
17
|
+
though the CLI had just told you `--force` would fix it. Now it asks:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
✗ remote code changed since last sync — pull first (or repeat with --force to overwrite the draft)
|
|
21
|
+
retry with --force and overwrite the remote draft? [y/N]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The default is **No**: a bare `Enter`, or anything other than `y`/`yes`,
|
|
25
|
+
returns to the menu exactly as before. Answering `y` re-runs the same menu row
|
|
26
|
+
(flags included) with `--force`, overwriting the n8n **draft** only — the
|
|
27
|
+
published version is untouched.
|
|
28
|
+
|
|
29
|
+
**It only appears for failures `--force` can actually fix.** A layout
|
|
30
|
+
compliance error never prompts, because forcing does not bypass it. And this
|
|
31
|
+
is the interactive picker session only: piped and non-interactive runs never
|
|
32
|
+
prompt — they print the `--force` hint and exit non-zero, unchanged.
|
|
33
|
+
|
|
34
|
+
- **`diff` — the new verb for "show me the actual changed lines".** It is the
|
|
35
|
+
promoted half of `status --diff`: per-node unified line diffs of your local
|
|
36
|
+
code against the n8n draft, `.ts` compiled first (bundling `shared/*`, so a
|
|
37
|
+
helper edit shows every importing node). Nodes that are in sync are omitted
|
|
38
|
+
entirely, and a clean tree says so in one line. Multi-ref like `pull`/`push`;
|
|
39
|
+
no workflow on a terminal opens the picker.
|
|
40
|
+
|
|
41
|
+
**It always exits 0.** `diff` is an inspection view, like `git diff` — the
|
|
42
|
+
gate is `preflight`. See the migration note under Removed.
|
|
43
|
+
|
|
44
|
+
- **`preflight --viewer`** (with `--simulate`): leaves a browsable throwaway
|
|
45
|
+
n8n running so you can open the replayed run in the UI — the interactive half
|
|
46
|
+
of the old `simulate` verb, now explicit instead of implied by a TTY. It does
|
|
47
|
+
**not** relax preflight's safety contract: the graded run stays headless with
|
|
48
|
+
`--network-none`, and the viewer is a second, separate container. A workflow
|
|
49
|
+
with a multi-batch loop reports `simulate` as **skipped** under `--viewer`
|
|
50
|
+
("a preview, not a pass/fail check"), never as a pass.
|
|
51
|
+
|
|
52
|
+
- **`preflight --no-typecheck`** skips the `types` check — the escape hatch the
|
|
53
|
+
retired `check` verb had.
|
|
54
|
+
|
|
55
|
+
- **The agent guard now logs a startup line and an audit trail** (`mcp connect`
|
|
56
|
+
and `mcp serve` alike, on stderr):
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
guard: connected to <host> — forwarding all n8n MCP tools, blocking jsCode writes in update_workflow
|
|
60
|
+
guard: forwarded search_workflows
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Previously the guard spoke **only** when it blocked something, so an empty log
|
|
64
|
+
meant either "ran, blocked nothing" or "never started" — indistinguishable,
|
|
65
|
+
and opposite in meaning. The startup line settles that; the per-call lines
|
|
66
|
+
make the guard the one place that can answer *what did an agent actually do
|
|
67
|
+
to my n8n instance?*, since every MCP call passes through it.
|
|
68
|
+
|
|
69
|
+
**Tool names only — arguments are never logged**, so the log stays safe to
|
|
70
|
+
attach to a bug report.
|
|
71
|
+
|
|
72
|
+
- **Every `preflight` finding can now carry `details[]`** — the full list behind
|
|
73
|
+
the one-line message: *every* layout violation, *every* `tsc` error, the
|
|
74
|
+
drifted node names, the viewer URL. Printed indented under the check line,
|
|
75
|
+
and present in `--json`. This is how the information `check` printed in full
|
|
76
|
+
survives its removal; without it, folding `check` into `preflight` would have
|
|
77
|
+
truncated a 12-violation layout failure to its first line.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- **The picker lists pulled workflows newest-synced first**, instead of the
|
|
82
|
+
folder's alphabetical order — the workflow you last pulled or pushed is under
|
|
83
|
+
the cursor when the picker opens. Unpulled remote rows keep their place after
|
|
84
|
+
the local ones. The signal is each workflow folder's sync timestamp, so it is
|
|
85
|
+
*local activity*, not committed history: right after a fresh `git clone`
|
|
86
|
+
everything looks equally recent and the list falls back to alphabetical until
|
|
87
|
+
your first pull or push. Scripted `list` output is deliberately unchanged.
|
|
88
|
+
|
|
89
|
+
- **The CLI banner's `n8n` wordmark now uses the brand orange, matching the
|
|
90
|
+
website** (`#E18528`, derived from the site's accent color) rather than ANSI
|
|
91
|
+
red. It degrades gracefully — a 256-color terminal gets the nearest orange, a
|
|
92
|
+
16-color one keeps the old red — and piped output and `NO_COLOR` stay plain,
|
|
93
|
+
exactly as before.
|
|
94
|
+
|
|
95
|
+
- **Breaking: `preflight`'s profiles are replaced by two orthogonal flags.**
|
|
96
|
+
`--full` and the `Profile` model are gone. Depth is now `--simulate`
|
|
97
|
+
(**additive** — appends the local-engine run of your code) and `--offline`
|
|
98
|
+
(**subtractive** — drops the instance-reads tier), and they compose:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
preflight static + instance reads (the default gate)
|
|
102
|
+
preflight --simulate + a local-engine run of your code
|
|
103
|
+
preflight --offline static only — no instance contact
|
|
104
|
+
preflight --offline --simulate static + local engine, no instance
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Migration:** `--full` → `--simulate`. And read the next entry carefully —
|
|
108
|
+
`--offline` still exists but means something narrower.
|
|
109
|
+
|
|
110
|
+
- **Breaking: `preflight --offline` no longer runs the local-engine replay.**
|
|
111
|
+
It used to mean "static + engine, no instance"; it now means "static only".
|
|
112
|
+
The flag name is unchanged, so **nothing will error** — an air-gapped CI job
|
|
113
|
+
on `preflight --offline` simply stops running the engine and quietly loses
|
|
114
|
+
that coverage. **Migration:** `preflight --offline --simulate` is the old
|
|
115
|
+
`--offline`. (This narrowing is also what makes `--offline` fast enough for
|
|
116
|
+
the per-edit hook: it now spawns no Docker container.)
|
|
117
|
+
|
|
118
|
+
- **Breaking: `preflight --json` replaces `profile` with `flags`.** Where the
|
|
119
|
+
report carried `"profile": "default" | "full" | "offline"` it now carries
|
|
120
|
+
`"flags": {"simulate": false, "offline": false}`. Agents key on this. Every
|
|
121
|
+
other field is unchanged, and each entry in `checks[]` gains an optional
|
|
122
|
+
`details: string[]`.
|
|
123
|
+
|
|
124
|
+
- **`preflight` with no workflow and an empty `"workflows"` config now checks
|
|
125
|
+
every *pulled* workflow** instead of erroring with "no workflow ids" — the
|
|
126
|
+
behaviour the `check` verb had, kept now that `preflight` absorbs it.
|
|
127
|
+
|
|
128
|
+
- **A workflow folder with an unreadable `.decanter.json` no longer fails your
|
|
129
|
+
gate.** `check` scanned *folders*, so a corrupt state file anywhere under
|
|
130
|
+
`workflows/` was a hard error for the whole run. `preflight` grades resolved
|
|
131
|
+
*workflows*, and a folder whose state won't parse can't resolve to one — so
|
|
132
|
+
it is named in a warning (`corrupt .decanter.json (…) — skipping this
|
|
133
|
+
folder`) and skipped, while every healthy workflow is still graded. The fact
|
|
134
|
+
is still reported; it just no longer blocks work on unrelated workflows.
|
|
135
|
+
|
|
136
|
+
- **`preflight --simulate` accepts multiple workflows.** The old `simulate`
|
|
137
|
+
verb took exactly one; preflight loops, so a multi-ref run spins one engine
|
|
138
|
+
container per workflow, serially.
|
|
139
|
+
|
|
140
|
+
- **The scaffolded template now runs `preflight --offline` where it ran
|
|
141
|
+
`check`** — the PostToolUse verify hook, both `package.json` scripts, and the
|
|
142
|
+
agent-facing prose in `AGENTS.md` / `CLAUDE.md`. *Existing sync dirs keep
|
|
143
|
+
their files*: re-run `n8n-decanter init` to be offered the refresh, and note
|
|
144
|
+
that init leaves locally-modified files alone, so a hand-edited hook or
|
|
145
|
+
`package.json` still invokes a removed verb until you update it yourself.
|
|
146
|
+
|
|
147
|
+
- The scaffolded agent permission allowlist swaps its `check`/`status`/
|
|
148
|
+
`simulate` rules for a `diff` pair (both the bare and `npx` shapes);
|
|
149
|
+
`preflight --simulate` is already covered by the existing `preflight:*` rule.
|
|
150
|
+
|
|
151
|
+
- The interactive picker's action menu is now `preflight`, `preflight
|
|
152
|
+
--simulate`, `diff`, `pull`, `push`, `watch`, `executions` — a menu row may
|
|
153
|
+
carry flags, which is how the browsable local-engine run survives the fold.
|
|
154
|
+
|
|
155
|
+
- **The scaffolded agent contract now treats `push` as part of finishing the
|
|
156
|
+
work, and reserves "ask the user first" for `publish`.** A push lands on the
|
|
157
|
+
workflow's **draft** and never changes what is running; only `publish` /
|
|
158
|
+
`push --publish` / `unpublish` do. The old rule gated both behind "only when
|
|
159
|
+
the user asks", so an agent handed "build me an hourly job that tags orders"
|
|
160
|
+
would build the structure in n8n, write and verify all the Code, and then
|
|
161
|
+
**stop** — leaving every Code node empty on the instance and the real code in
|
|
162
|
+
the repo, reporting "ready to push". Correct by the old rule, and not what
|
|
163
|
+
anybody asked for. Agents are now told to push once offline checks pass, to
|
|
164
|
+
say what landed, and to still ask first when the workflow is published/active
|
|
165
|
+
or a teammate is editing it. Going live remains a deliberate, user-requested
|
|
166
|
+
step. Affects `template/AGENTS.md.example` (copied into new sync dirs by
|
|
167
|
+
`init`) and the `/docs/agents` surfaces. *(Surfaced by the Plan 35 blind field
|
|
168
|
+
test, where the "failing" agent was following the old contract exactly.)*
|
|
169
|
+
|
|
170
|
+
- **The scaffolded agent contract now follows the `preflight → push → test →
|
|
171
|
+
publish` flow.** `preflight` is local-only (it no longer runs on the instance),
|
|
172
|
+
so `test` — which runs the workflow's **draft** — is only meaningful *after* a
|
|
173
|
+
push. The old contract framed `test` as a pre-push runtime check and ended its
|
|
174
|
+
loop at `preflight → push`; both are now reconciled to the new order. Affects
|
|
175
|
+
`template/AGENTS.md.example` and the `/docs/agents` surfaces. *(Same surface
|
|
176
|
+
the Plan 60 verb reorder changed — kept in lockstep so the blind field test
|
|
177
|
+
grades agents against a contract that matches the tool.)*
|
|
178
|
+
|
|
179
|
+
- **Breaking: `preflight` no longer runs the instance-side `test` stage.** It
|
|
180
|
+
ran `test_workflow` against n8n's **draft**, while every other stage graded
|
|
181
|
+
your **local files** — so whenever a push was pending, one score described
|
|
182
|
+
two different versions of the workflow, flagged only by a `-10` parity warn.
|
|
183
|
+
A report could read *caution, 90/100* while its runtime evidence was about
|
|
184
|
+
code you weren't shipping. `preflight` now grades local code only; the
|
|
185
|
+
instance is read for sync facts and never executed.
|
|
186
|
+
|
|
187
|
+
The documented flow is **`preflight` → `push` → `test` → `publish`**: verify
|
|
188
|
+
local code, make it the draft, run what you actually pushed, then go live.
|
|
189
|
+
Nothing was removed from the toolbox — the instance run moved to where it
|
|
190
|
+
means something.
|
|
191
|
+
|
|
192
|
+
**Migration:** a CI job that gates on a plain `preflight` no
|
|
193
|
+
longer gets an instance run inside that gate — the draft is never executed by
|
|
194
|
+
preflight. To keep instance-run coverage, add `n8n-decanter test` as its own
|
|
195
|
+
step **after** your push step (`--require=test` users get a hard error with
|
|
196
|
+
this guidance; default-profile users get this note).
|
|
197
|
+
|
|
198
|
+
- **Breaking: `preflight --require=test` is rejected**, with a message pointing
|
|
199
|
+
at the new flow rather than a bare "unknown check". The `test` id is gone
|
|
200
|
+
from `--require`, from `--json` `checks[]`, and from `coverage`.
|
|
201
|
+
|
|
202
|
+
- **`preflight` auto-fetches a capture only under `--simulate`** — the flag
|
|
203
|
+
that adds the one stage which consumes a capture. Without it there is no
|
|
204
|
+
runtime stage, so preflight no longer fetches, and a missing or stale capture
|
|
205
|
+
is reported as `info` rather than `warn` — nothing would consume one
|
|
206
|
+
(`--offline` never reaches the instance to fetch either way).
|
|
207
|
+
|
|
208
|
+
- The `parity` warn is reworded. It was a caveat about the runtime tier
|
|
209
|
+
grading the wrong artifact; that's no longer possible, so it is now the plain
|
|
210
|
+
next step: *"local code differs from the draft in N node(s) — push to make it
|
|
211
|
+
the draft, then test"*.
|
|
212
|
+
|
|
213
|
+
- **The local-engine replay is the sole runtime stage** (`preflight --simulate`)
|
|
214
|
+
and needs Docker. For runtime evidence without Docker, push and then run
|
|
215
|
+
`test`.
|
|
216
|
+
|
|
217
|
+
- **Node-file type checking moved off TypeScript's legacy `node10` module
|
|
218
|
+
resolution** to `moduleResolution: "bundler"` (with `module: "preserve"`), in
|
|
219
|
+
the scaffolded `tsconfig.json`. This matches what push actually does — `.ts`
|
|
220
|
+
nodes are compiled with esbuild in bundling mode — and keeps the documented
|
|
221
|
+
extensionless import style working (`import { total } from
|
|
222
|
+
"../../shared/money"`). It also unblocks TypeScript 6, which turns `node10`
|
|
223
|
+
into a hard error (`TS5107`), and TypeScript 7, which removes it outright.
|
|
224
|
+
`node16`/`nodenext` were **not** chosen: they reject extensionless relative
|
|
225
|
+
imports (`TS2835`) and would force every node file to be rewritten with `.js`
|
|
226
|
+
extensions. No change to which Node.js versions are supported. *Existing sync
|
|
227
|
+
dirs keep their current `tsconfig.json`* — re-run `n8n-decanter init` to be
|
|
228
|
+
offered the refresh; if you have hand-edited yours (or created it before
|
|
229
|
+
template baselines existed), init reports it and leaves it alone, so apply
|
|
230
|
+
the two-line change yourself when you move to TypeScript 6+.
|
|
231
|
+
|
|
232
|
+
### Removed
|
|
233
|
+
|
|
234
|
+
- **Breaking: the `check`, `status`, and `simulate` verbs.** All three were
|
|
235
|
+
variations on "check my thing", and telling them apart was the single most
|
|
236
|
+
confusing part of the surface. They fold into two:
|
|
237
|
+
|
|
238
|
+
| You used to run | Now run |
|
|
239
|
+
| --- | --- |
|
|
240
|
+
| `check [workflow…]` | `preflight --offline [workflow…]` |
|
|
241
|
+
| `check --no-typecheck` | `preflight --offline --no-typecheck` |
|
|
242
|
+
| `status [workflow…]` | `preflight [workflow…]` (the scored summary) |
|
|
243
|
+
| `status --diff` | `diff [workflow…]` (the changed lines) |
|
|
244
|
+
| `simulate <workflow>` | `preflight <workflow> --offline --simulate` |
|
|
245
|
+
| `simulate --network-none` | `preflight --simulate` (always network-none) |
|
|
246
|
+
|
|
247
|
+
Each removed verb exits non-zero naming its replacement, so a stale script
|
|
248
|
+
fails loudly rather than silently. **Note the `simulate` row:** the verb
|
|
249
|
+
needed no credentials, and a bare `preflight --simulate` still runs the
|
|
250
|
+
instance tier — add `--offline` for the credential-free equivalent.
|
|
251
|
+
|
|
252
|
+
Nothing was lost with the verbs. The compliance guard still gates every push
|
|
253
|
+
and watch save (only the standalone *view* is gone, and preflight's `layout`
|
|
254
|
+
finding now lists every violation); the publish state, live-lags-draft note
|
|
255
|
+
and snapshot-stale hint are preflight's `lifecycle` and `snapshot` findings.
|
|
256
|
+
|
|
257
|
+
- **Breaking: `status`'s CI exit codes.** `status` exited 1 on a code conflict
|
|
258
|
+
or remote drift, and pipelines gated on that. `diff` always exits 0.
|
|
259
|
+
**Migration:** gate on `preflight`, whose `drift` check **fails** on a
|
|
260
|
+
CONFLICT and warns on remote drift (add `--fail-on=warn` to gate on the warn
|
|
261
|
+
too).
|
|
262
|
+
|
|
263
|
+
- **`simulate --network-none`.** `preflight` always runs the graded engine
|
|
264
|
+
replay with no network, so the flag had nothing left to turn off.
|
|
265
|
+
|
|
266
|
+
- **Breaking: the `preflight --quick` profile.** With the `test` stage gone it
|
|
267
|
+
was byte-identical to the default profile, and rather than redefine it into a
|
|
268
|
+
meaning users would have to learn and then unlearn, it is gone. Static-only
|
|
269
|
+
checking is now `preflight --offline`; the local-engine replay is
|
|
270
|
+
`preflight --simulate` (see the profile→flags entry above, which retired
|
|
271
|
+
`--full` and the whole profile vocabulary in the same release).
|
|
272
|
+
|
|
273
|
+
**Neither `--quick` nor `--full` is recognized any more — they are simply
|
|
274
|
+
gone, not rejected with a migration.** The CLI ignores flags it does not
|
|
275
|
+
know, so `preflight --full` now runs the *default* gate (no engine) and
|
|
276
|
+
`preflight --quick` runs it too, both exiting 0. **If you have either in a CI
|
|
277
|
+
job, update it in the same step as this upgrade** — nothing will tell you at
|
|
278
|
+
runtime. `--full` → `--simulate`; `--quick` → `--offline`.
|
|
279
|
+
- **Breaking: `preflight --trigger <node>`.** It existed only to feed the
|
|
280
|
+
removed instance `test` stage; since that stage's removal it parsed and did
|
|
281
|
+
nothing. `test --trigger <node>` (the post-push instance run) keeps the flag
|
|
282
|
+
— that is where trigger selection acts.
|
|
283
|
+
|
|
284
|
+
### Fixed
|
|
285
|
+
|
|
286
|
+
- **Renaming a `.ts` node in n8n no longer leaves it stuck on "push pending".**
|
|
287
|
+
A `.ts` node that imports from `shared/` (or an opted-in npm package) is
|
|
288
|
+
compiled with esbuild's bundler, and esbuild labels every bundled module with
|
|
289
|
+
a `// <path>` comment. That label used the node's **own filename**, so it
|
|
290
|
+
landed inside the compiled bytes — and inside the `@ts-n8n sha256:` marker.
|
|
291
|
+
Rename the node in n8n, `pull` renames `compute.ts` → `ümläut-nödé.ts`, and
|
|
292
|
+
the artifact changed even though **not one line of your source did**: `diff`
|
|
293
|
+
reported a difference that was purely the comment line, and the node read
|
|
294
|
+
"local changes — push pending" until you pushed a no-op. The entry label is
|
|
295
|
+
now a fixed name, so a pure rename round-trip is byte-stable and comes back
|
|
296
|
+
clean.
|
|
297
|
+
|
|
298
|
+
**One-time effect when you upgrade:** because the compiled bytes changed, the
|
|
299
|
+
first `diff`/`pull` after upgrading reports **"modified, not yet pushed"** for
|
|
300
|
+
every `.ts` node **that has imports** — a comment-line difference only. One
|
|
301
|
+
`push` per workflow clears it, and it is a plain push, not a conflict: the
|
|
302
|
+
remote code is untouched, so the drift guard does not trip and `--force` is
|
|
303
|
+
not needed. `.ts` nodes **without** imports compile through a different path
|
|
304
|
+
that never embedded the name, and `.js` nodes are unaffected.
|
|
305
|
+
|
|
306
|
+
- **The scaffolded MCP guard now starts under a *local* install, not only a
|
|
307
|
+
global one.** `init`'s `.mcp.json` / `opencode.json` spawned the guard as a
|
|
308
|
+
bare `n8n-decanter mcp connect`, which only resolves when the CLI is on the
|
|
309
|
+
agent's `PATH` — i.e. a **global** install. With decanter installed as a
|
|
310
|
+
**local** project dependency the command silently failed to start, so the
|
|
311
|
+
agent got no guarded route and fell back to whatever other n8n MCP it had,
|
|
312
|
+
unguarded. The scaffolded command is now `npx --no-install n8n-decanter mcp
|
|
313
|
+
connect`, which resolves the local `node_modules` bin **and** a global
|
|
314
|
+
install; `--no-install` never downloads from npm, so a genuinely missing
|
|
315
|
+
install fails loudly instead of silently. (Plan 58.)
|
|
316
|
+
|
|
317
|
+
- **The scaffolded agent permissions now cover `npx n8n-decanter …` — including
|
|
318
|
+
the `push --force` denial.** The same local-install gap applies to the CLI
|
|
319
|
+
calls an agent makes in a shell: under a local (devDependency) install a bare
|
|
320
|
+
`n8n-decanter <verb>` is not on `PATH`, so the working form is
|
|
321
|
+
`npx n8n-decanter <verb>`. The permission matcher keys on the command prefix,
|
|
322
|
+
so that form previously matched **neither** the allow rules (every safe call
|
|
323
|
+
would stop to ask) **nor the `push --force` deny rule** — meaning the
|
|
324
|
+
force-push guard rail could be sidestepped simply by invoking through `npx`.
|
|
325
|
+
Both lists now carry the `npx` forms (Claude Code `settings.json` and
|
|
326
|
+
opencode), and the scaffolded `AGENTS.md` tells agents to add the prefix when
|
|
327
|
+
the bare command is not found. Installing globally is **not** required — a
|
|
328
|
+
per-sync-dir devDependency remains fully supported. (Plan 58.)
|
|
329
|
+
|
|
330
|
+
## [0.7.0] - 2026-07-24
|
|
331
|
+
|
|
332
|
+
### Added
|
|
333
|
+
|
|
334
|
+
- **`check` now warns when local work has not been registered with n8n** — a
|
|
335
|
+
node whose `//@file:` placeholder has moved off what `.decanter.json` records
|
|
336
|
+
(the shape of a `.js`→`.ts` conversion), or whose recorded file is gone from
|
|
337
|
+
disk. It stays a *warning*, not an error: `push` reconciles the file map, and
|
|
338
|
+
the compliance guard runs before that reconcile, so failing here would refuse
|
|
339
|
+
the one command that fixes it.
|
|
340
|
+
|
|
341
|
+
- **`n8n-decanter --version` prints the installed version** (`-v` too), the way
|
|
342
|
+
every CLI is expected to. It answers before any config load or verb dispatch,
|
|
343
|
+
so it works from anywhere — including outside a sync dir. Passed *alongside* a
|
|
344
|
+
verb it is a hard error naming the flag you meant, so a stray `--version`
|
|
345
|
+
can't quietly swallow a command.
|
|
346
|
+
|
|
347
|
+
- **A first `init` points at n8n's official skills pack.** Setup now closes by
|
|
348
|
+
naming [n8n-io/skills](https://github.com/n8n-io/skills) — the knowledge layer
|
|
349
|
+
that makes agentic workflow building work — and printing the install commands
|
|
350
|
+
for **Claude Code**, **Codex**, and **skills.sh**, with the agent it detects
|
|
351
|
+
from your environment listed first and the activation step each one still
|
|
352
|
+
needs. It prints; it does not install: that would mean spawning a third-party
|
|
353
|
+
CLI to mutate agent state outside the sync dir, and a plugin installed
|
|
354
|
+
mid-session isn't active until the agent reloads. Said once per sync dir (no
|
|
355
|
+
re-init repeats it), on every path including piped and `--host`-driven runs,
|
|
356
|
+
and it consumes no input — no existing script's stdin changes. *(Plan 55.)*
|
|
357
|
+
|
|
358
|
+
- **`init` can run non-interactively via `--host` / `--token` / `--api-key`.**
|
|
359
|
+
Passing any of them drives setup purely from the flags plus any existing
|
|
360
|
+
`.env` and issues **no prompt** — so a script or coding agent can bootstrap a
|
|
361
|
+
sync dir without the interactive stdin dance (the field-test agents needed
|
|
362
|
+
20+ tries to drive the old prompt path). `--host` is required in this mode
|
|
363
|
+
(a scheme-less local host is normalized to `http://`, like a typed one) and
|
|
364
|
+
wins over an existing `.env` value; `--token` sets `N8N_MCP_TOKEN` (headless
|
|
365
|
+
OAuth is still terminal-only); `--api-key` sets the optional `N8N_API_KEY`.
|
|
366
|
+
The flag-less path (interactive, or piped answers) is unchanged. *(Plan 35
|
|
367
|
+
field-test finding.)*
|
|
368
|
+
|
|
369
|
+
- **`node run` now emulates `$jmespath`.** A Code node that calls
|
|
370
|
+
`$jmespath(data, expr)` (or the `$jmesPath` alias) runs offline, matching
|
|
371
|
+
n8n's result (backed by `jmespath@0.16.0`, the version n8n pins). It also
|
|
372
|
+
fills in `$items()`/`$node` (views over the fixture's `nodes`), `$vars`/
|
|
373
|
+
`$secrets` (new fixture fields), and `$nodeId`/`$nodeVersion`/`$webhookId`.
|
|
374
|
+
|
|
375
|
+
- **`node run` fixtures gained `vars` and `secrets`** to pin the instance-scoped
|
|
376
|
+
`$vars`/`$secrets` when a node reads them.
|
|
377
|
+
|
|
378
|
+
### Changed
|
|
379
|
+
|
|
380
|
+
- **Breaking:** the scaffolded Claude Code settings moved from
|
|
381
|
+
**`.claude/settings.local.json` to `.claude/settings.json`**, and `init` now
|
|
382
|
+
migrates existing sync dirs. The file holds *project policy* — decanter's verb
|
|
383
|
+
permissions plus the `verify.mjs` and `mcp-route-check.mjs` hooks — with
|
|
384
|
+
nothing machine-specific in it; it was already being committed and tracked in
|
|
385
|
+
the shared `.decanter-template.json`, so `local` was the wrong scope, and it
|
|
386
|
+
squatted the one file Claude Code reserves for **your** own overrides. The
|
|
387
|
+
local slot is now yours: permission lists merge across the two files and a
|
|
388
|
+
`deny` beats an `allow`, so a local file can add to the policy but cannot
|
|
389
|
+
unblock what the project denies. On re-init, an untouched copy is moved for
|
|
390
|
+
you; a copy you edited is left exactly where it is and the new file is **not**
|
|
391
|
+
written (both would register their hooks) — `init` says what to move, and
|
|
392
|
+
`--force` resolves it by removing the old file. A `settings.local.json` that
|
|
393
|
+
`init` never wrote is never touched. *(Plan 56.)*
|
|
394
|
+
|
|
395
|
+
- **Breaking: `backup restore` takes the backup as an argument, not a flag —
|
|
396
|
+
`backup restore <workflow> [<backup>]`.** `--version <id>` and `--at <ts>`
|
|
397
|
+
are gone. The argument is a **backup ref** resolved by shape, exactly like a
|
|
398
|
+
`<workflow>` ref: paste a timestamp (or a prefix — a bare date is enough) or a
|
|
399
|
+
`versionId` (short or full), whichever column of `backup list` you have to
|
|
400
|
+
hand. `backup restore order-sync 2026-07-24` and `backup restore order-sync
|
|
401
|
+
a1b2c3d4` both just work; a ref that matches nothing is an error, never a
|
|
402
|
+
silent fall back to the latest. The retired flags fail loudly with the
|
|
403
|
+
replacement. This also un-squats `--version`, which no CLI can spend on a
|
|
404
|
+
verb-scoped meaning (see Added).
|
|
405
|
+
|
|
406
|
+
- **`check`'s success line now states its scope** —
|
|
407
|
+
`OK (local layout — status compares with n8n)` instead of a bare `OK`. `check`
|
|
408
|
+
is offline by definition, so green means "well-formed", never "live in n8n".
|
|
409
|
+
The agent guide gained the matching rule, and the `.js`→`.ts` recipe now ends
|
|
410
|
+
at `push` rather than `check` — it previously told you to stop one step short
|
|
411
|
+
of the conversion actually reaching the instance. *(Surfaced by the Plan 35
|
|
412
|
+
blind field test: three separate sessions authored code, read a green `check`
|
|
413
|
+
as "done", and never pushed.)*
|
|
414
|
+
|
|
415
|
+
- **`scenario create` / `scenario check` with no workflow now open the picker on
|
|
416
|
+
a terminal**, like every other ref-taking verb (`pull`, `push`, `backup …`, …).
|
|
417
|
+
They previously hard-errored with a usage line even on a TTY, which made them
|
|
418
|
+
the odd ones out. Piped / non-TTY runs are unchanged — still the usage error —
|
|
419
|
+
so scripts and agent harnesses never block on a prompt. *(Surfaced by the Plan
|
|
420
|
+
35 blind field test, where an agent tripped the inconsistency twice.)*
|
|
421
|
+
|
|
422
|
+
- **`node run` signposts instead of crashing on instance-scoped globals.** A
|
|
423
|
+
global whose value lives on the running instance (`$vars`/`$secrets` when
|
|
424
|
+
unpinned, `$evaluateExpression`) now throws a friendly message that names the
|
|
425
|
+
global and points to `test` (or the fixture field) — never a bare
|
|
426
|
+
`ReferenceError`. `docs/cli/node-run.md` documents the covered / partial /
|
|
427
|
+
unsupported boundary.
|
|
428
|
+
|
|
429
|
+
### Fixed
|
|
430
|
+
|
|
431
|
+
- **The documented Claude Code skills-install commands are no longer
|
|
432
|
+
copy-paste-broken.** `/plugin marketplace add` / `/plugin install` are
|
|
433
|
+
in-session slash commands, but the docs and the scaffolded `AGENTS.md`
|
|
434
|
+
presented them as shell commands. Both now show the in-session form and the
|
|
435
|
+
real shell equivalents (`claude plugin marketplace add …` /
|
|
436
|
+
`claude plugin install …`) separately, plus the post-install activation step
|
|
437
|
+
each agent needs.
|
|
438
|
+
|
|
439
|
+
- **A `.js`→`.ts` conversion is no longer reverted by a pull that fires before
|
|
440
|
+
the first TS push.** Re-pointing a node's `//@file:` placeholder to a `.ts`
|
|
441
|
+
file and swapping the source is the sanctioned way to convert a node, but a
|
|
442
|
+
`pull` landing in the window before the first TS `push` — notably the
|
|
443
|
+
on-by-default live-mirror background refresh after a structure edit — rewrote
|
|
444
|
+
the placeholder back to `.js` and left `.decanter.json` pointing at the
|
|
445
|
+
deleted `.js` file, so the next push failed with `referenced node file
|
|
446
|
+
missing`. Pull now honors the re-pointed placeholder exactly as push does
|
|
447
|
+
(they share one reconcile step). *(Plan 35 field-test finding.)*
|
|
448
|
+
|
|
449
|
+
- **`init` no longer breaks local `http` instances.** A scheme-less host typed
|
|
450
|
+
at the `n8n host:` prompt now defaults to `http://` for local addresses
|
|
451
|
+
(`localhost`, loopback, private LAN ranges, `*.local`) and `https://`
|
|
452
|
+
otherwise. Previously every scheme-less host got `https://`, so a local n8n
|
|
453
|
+
(plain http) was written to `.env` as a TLS URL and every sync/guard call
|
|
454
|
+
failed with `fetch failed`. A scheme you type is still kept as-is.
|
|
455
|
+
|
|
456
|
+
- **`n8n-globals.d.ts` no longer over-declares `$if`/`$min`/`$max`.** Those are
|
|
457
|
+
n8n *expression-language* helpers (`{{ }}` only), not Code-node globals — they
|
|
458
|
+
throw in a real Code node too — so declaring them wrongly type-checked broken
|
|
459
|
+
code. The declared surface now matches what a Code node actually sees, and is
|
|
460
|
+
single-sourced (init copies the one root file — no duplicate template copy).
|
|
461
|
+
|
|
462
|
+
- The scaffolded agent permission allowlist (`.claude/settings.json` — see the
|
|
463
|
+
move under Changed) now
|
|
464
|
+
pre-approves the read-only **`preflight`** gate, so an agent following the
|
|
465
|
+
template's recommended `edit → check → preflight → push` loop no longer stalls
|
|
466
|
+
on a permission prompt at the gate itself. Also dropped the obsolete
|
|
467
|
+
`*.remote.js` deny rule — those artifacts were removed in the Plan 32 MCP pivot.
|
|
468
|
+
|
|
10
469
|
## [0.6.0] - 2026-07-23
|
|
11
470
|
|
|
12
471
|
### Added
|
package/README.md
CHANGED
|
@@ -35,17 +35,22 @@ clean git diffs.
|
|
|
35
35
|
- **TypeScript or typed JS** — write nodes in TS (compiled on push) or plain
|
|
36
36
|
JS; n8n globals (`$input`, `$('…')`, …) are typed in both.
|
|
37
37
|
- **Agent-native** — `init` scaffolds Claude Code / Cursor / Codex configs;
|
|
38
|
-
offline `
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
the offline `preflight --offline` / `node run` loop gives agents a
|
|
39
|
+
credential-free feedback loop.
|
|
40
|
+
- **One gate, two depth flags** — a compliance guard, typecheck gate, and
|
|
41
|
+
per-node drift guard gate every push; `preflight` folds layout, types and
|
|
42
|
+
instance sync facts into a single **read-only, CI-gateable verdict** (never
|
|
43
|
+
writes, never runs on your instance). `--simulate` *adds* an offline engine
|
|
44
|
+
replay (Docker), `--offline` *drops* the instance reads — and both that
|
|
45
|
+
replay and instance-side `test` diff every node against a real captured
|
|
46
|
+
execution and exit 1 on divergence. The flow is **`preflight` → `push` →
|
|
47
|
+
`test` → `publish`**: verify local, make it the draft, run what you pushed,
|
|
48
|
+
go live.
|
|
45
49
|
- **Committed, schema-scaffolded scenarios** — `scenario create` turns a
|
|
46
50
|
captured execution and/or the workflow's own output schemas
|
|
47
51
|
(`--scaffold`, no LLM API) into a reviewable, git-tracked pin-data set that
|
|
48
|
-
`test
|
|
52
|
+
`test` and `preflight --simulate` replay — the durable counterpart to an
|
|
53
|
+
agent's ephemeral
|
|
49
54
|
in-session pin flow.
|
|
50
55
|
- **Live editing** — `watch` pushes on save; the open n8n editor tab reflects
|
|
51
56
|
each push live (n8n's own draft-edit refresh, no proxy needed).
|
|
@@ -87,7 +92,7 @@ On a terminal, `pull` with no argument lists your n8n workflows — pick one and
|
|
|
87
92
|
it lands in `workflows/<slug>/`. Know the id already? `n8n-decanter pull
|
|
88
93
|
<id-or-name>` pulls it directly (scriptable, no TTY needed).
|
|
89
94
|
(`decanter.config.json`'s optional `"workflows"` array just sets the default
|
|
90
|
-
set a bare `pull`/`push`/`
|
|
95
|
+
set a bare `pull`/`push`/`diff` acts on.)
|
|
91
96
|
|
|
92
97
|
**Credentials:** OAuth by default (via `init`); `N8N_MCP_TOKEN` for
|
|
93
98
|
headless/CI; `N8N_API_KEY` is optional, needed only for `executions`,
|
|
@@ -105,29 +110,31 @@ through [`mcp connect`](docs/cli/mcp-connect.md), a guard that forwards
|
|
|
105
110
|
everything except writes to a Code node's `jsCode` — those redirect back to
|
|
106
111
|
the repo (edit the file, `push`). No secret to manage.
|
|
107
112
|
|
|
113
|
+
**A first [`init`](docs/cli/init.md) prints the install commands** for Claude
|
|
114
|
+
Code, Codex, or any skills.sh-supported agent, listing the one it detects first.
|
|
115
|
+
|
|
108
116
|
**Full guide: [Using n8n's official skills](docs/agents/n8n-skills.md).**
|
|
109
117
|
|
|
110
118
|
## Commands
|
|
111
119
|
|
|
112
|
-
Bare `n8n-decanter` (no verb) opens an interactive picker on a terminal
|
|
120
|
+
Bare `n8n-decanter` (no verb) opens an interactive picker on a terminal —
|
|
121
|
+
newest-synced workflow first.
|
|
113
122
|
Full flag reference: `n8n-decanter --help`, or the
|
|
114
123
|
[command overview](docs/cli/overview.md).
|
|
115
124
|
|
|
116
125
|
| Verb | What it does |
|
|
117
126
|
|---|---|
|
|
118
|
-
| `init [dir]` |
|
|
127
|
+
| `init [dir]` | Bootstrap a sync dir — OAuth/token, template, config, agent tooling (`--host`/`--token`/`--api-key` drive it non-interactively) |
|
|
119
128
|
| `completion zsh\|bash` | Print a shell completion script |
|
|
120
129
|
| `pull [workflow…]` | Code + structure snapshot → `workflows/<kebab>/` |
|
|
121
130
|
| `push [workflow…]` | Push Code-node source to the workflow's **draft** |
|
|
122
131
|
| `watch [workflow]` | Push on save (draft-only); editor updates live |
|
|
123
132
|
| `publish` / `unpublish [workflow…]` | Take the draft live / back to draft-only |
|
|
124
|
-
| `
|
|
125
|
-
| `check [workflow…]` | Offline layout-compliance check + typecheck |
|
|
133
|
+
| `diff [workflow…]` | Per-node line diff of local code vs the n8n draft — an inspection view, always exits 0 |
|
|
126
134
|
| `executions [workflow…]` | Fetch recent execution data (read-only) |
|
|
127
135
|
| `data-tables [table…]` | Fetch data-table schema + rows (read-only) |
|
|
128
136
|
| `test <workflow>` | Pinned run **on the instance** (draft); diffs vs a capture |
|
|
129
|
-
| `
|
|
130
|
-
| `preflight [workflow…]` | The whole verification ladder as one scored, read-only gate (exits 1 on *not ready*) |
|
|
137
|
+
| `preflight [workflow…]` | The one local gate — layout, types, instance sync facts, optional local-engine replay — scored and read-only; run before `push` (exits 1 on *not ready*) |
|
|
131
138
|
| `scenario create` / `scenario check` | Build / validate a committed scenario (captured and/or schema-scaffolded) |
|
|
132
139
|
| `backup create` / `restore` / `list` | Git-native disaster recovery — capture / redeploy / list versioned full-export backups |
|
|
133
140
|
| `list [--remote]` | Pulled workflows (`--remote` adds unpulled ones) |
|
|
@@ -167,13 +174,13 @@ whole-workflow authoring toolkit.
|
|
|
167
174
|
| **Shared types & helpers in Code nodes** | ❌ self-host `NODE_FUNCTION_ALLOW_*` only; no libraries | ❌ not part of its model | ✅ `shared/*.ts` + npm bundled into self-contained nodes (Cloud-safe) |
|
|
168
175
|
| **Code as individual files** | ❌ no source files (JSON blob) | 🟡 one `.workflow.ts` per workflow | ✅ folder per workflow; each Code node its own `.js`/`.ts` |
|
|
169
176
|
| **Code-level git versioning** | 🟡 in-app history (DB snapshots, tiered retention); Git source control is Enterprise-only | ✅ GitOps sync of workflow source | ✅ real git — diffs, PRs, blame per Code node; auto-commit each sync (+ read-only structure snapshot) |
|
|
170
|
-
| **Preflights** (`
|
|
177
|
+
| **Preflights** (`preflight`, then `test`) | 🟡 re-run past executions / pin data, but online in-editor | 🟡 inspect executions against a live env | ✅ `preflight` scores your **local** code (layout, types, instance reads, optional offline `--simulate` engine replay) into one read-only, CI-gateable verdict *before* the push — then instance-side `test` runs the pushed draft; each runtime check diffs every node vs a real capture, exits 1 on divergence |
|
|
171
178
|
| **Draft-first code sync** | ✅ editor *Save* vs *Publish* (manual, in-browser) | 🟡 API sync republishes on push (no draft-only) | ✅ pushes land on the **draft**; `publish` is the deliberate go-live (over MCP) |
|
|
172
179
|
| **Live editing** | ✅ the canvas (baseline) | 🟡 explicit pull/push, no auto-watch | ✅ `watch`: push on save; the open editor tab reflects each push live (n8n-native) |
|
|
173
|
-
| **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 incl. a pre-wired `mcp connect` guard holding the credentials; offline `
|
|
180
|
+
| **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 incl. a pre-wired `mcp connect` guard holding the credentials; offline `preflight --offline`/`node run` loop |
|
|
174
181
|
| **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 |
|
|
175
182
|
| **Agentic workflow creation** | 🟡 AI Workflow Builder (natural language), but Cloud / plan-gated — credits, self-host needs setup | ✅ 537 node schemas + 7,700+ templates + skills | ✅ your agent builds structure over n8n's MCP (through the pre-wired `mcp connect` guard); decanter owns the Code-node source (files + `push`) |
|
|
176
|
-
| **Whole-workflow authoring** | ❌ | ✅ `.workflow.ts` decorator classes (structure + links) |
|
|
183
|
+
| **Whole-workflow authoring** | ❌ | ✅ `.workflow.ts` decorator classes (structure + links) | 🟡 Code-node source only; your agent builds structure live over MCP (row above), mirrored to a read-only `workflow.json` |
|
|
177
184
|
| **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) |
|
|
178
185
|
|
|
179
186
|
Legend: ✅ first-class · 🟡 partial or indirect · ❌ not supported.
|
package/dist/lib/backup.mjs
CHANGED
|
@@ -156,17 +156,17 @@ async function assembleForRestore(dir, backup, log) {
|
|
|
156
156
|
return { body, nodes };
|
|
157
157
|
}
|
|
158
158
|
/**
|
|
159
|
-
* `backup restore` — git → instance. Select a backup (latest by default;
|
|
160
|
-
*
|
|
159
|
+
* `backup restore` — git → instance. Select a backup (latest by default; a
|
|
160
|
+
* positional backup ref, or a TTY chooser), re-inline the Code-node source,
|
|
161
161
|
* compliance-guard the assembly, and REST-POST a NEW, unpublished workflow
|
|
162
162
|
* (new workflow id; node ids preserved). Prints credential-rebind hints + the
|
|
163
163
|
* editor URL; publish is the operator's next step.
|
|
164
164
|
*/
|
|
165
|
-
export async function backupRestore(api, dir, { host,
|
|
165
|
+
export async function backupRestore(api, dir, { host, ref, interactive = false }, log) {
|
|
166
166
|
const backups = listBackups(dir);
|
|
167
167
|
if (backups.length === 0)
|
|
168
168
|
throw new Error(`no backups in ${path.basename(dir)}/${BACKUPS_DIR}/ — run \`backup create\` first`);
|
|
169
|
-
const selected = await selectBackup(backups, {
|
|
169
|
+
const selected = await selectBackup(backups, { ref, interactive }, log);
|
|
170
170
|
if (selected === null)
|
|
171
171
|
return null; // user quit the chooser
|
|
172
172
|
const backup = JSON.parse(readFileSync(selected.file, "utf8"));
|
|
@@ -187,28 +187,38 @@ export async function backupRestore(api, dir, { host, version, at, interactive =
|
|
|
187
187
|
log.info(style.dim("it landed unpublished — review, rebind credentials, then publish to go live"));
|
|
188
188
|
return { id: created.id };
|
|
189
189
|
}
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return
|
|
190
|
+
/**
|
|
191
|
+
* A **backup ref** resolves like a workflow ref does — untyped, by shape, over
|
|
192
|
+
* the two keys `backup list` prints. Both are in the filename
|
|
193
|
+
* (`<timestamp>.<shortVersionId>.json`), so one positional covers both without
|
|
194
|
+
* the caller having to say which it meant:
|
|
195
|
+
*
|
|
196
|
+
* - **timestamp** — exact, or any prefix of the filename (a bare date works)
|
|
197
|
+
* - **versionId** — the short one in the filename, the full one the user pastes
|
|
198
|
+
* from n8n (a prefix of which the short id is the head), or the full one
|
|
199
|
+
* stored *inside* the backup
|
|
200
|
+
*
|
|
201
|
+
* The two key spaces don't collide in practice (timestamps lead with a date,
|
|
202
|
+
* versionIds are uuids), so first-match-wins needs no tie-break.
|
|
203
|
+
*/
|
|
204
|
+
function matchesBackupRef(b, ref) {
|
|
205
|
+
if (b.timestamp === ref || b.name.startsWith(ref))
|
|
206
|
+
return true;
|
|
207
|
+
if (b.versionId === ref || ref.startsWith(b.versionId))
|
|
208
|
+
return true;
|
|
209
|
+
try {
|
|
210
|
+
return JSON.parse(readFileSync(b.file, "utf8")).versionId === ref;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
207
214
|
}
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
}
|
|
216
|
+
/** Pick which backup to restore: an explicit ref, else a TTY chooser, else latest. */
|
|
217
|
+
async function selectBackup(backups, { ref, interactive }, log) {
|
|
218
|
+
if (ref !== undefined) {
|
|
219
|
+
const hit = backups.find((b) => matchesBackupRef(b, ref));
|
|
210
220
|
if (!hit)
|
|
211
|
-
throw new Error(`no backup
|
|
221
|
+
throw new Error(`no backup matching "${ref}" in ${BACKUPS_DIR}/ — pass a timestamp or versionId from \`backup list\``);
|
|
212
222
|
return hit;
|
|
213
223
|
}
|
|
214
224
|
const latest = backups.at(-1);
|