n8n-guard 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 flow-84
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # n8n-guard
2
+
3
+ [![npm](https://img.shields.io/npm/v/n8n-guard.svg)](https://www.npmjs.com/package/n8n-guard)
4
+ [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.flow--84%2Fn8n--guard-blue)](https://registry.modelcontextprotocol.io/v0/servers?search=n8n-guard)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)
7
+
8
+ An MCP server for the operational layer *underneath* the n8n Public API.
9
+
10
+ Every n8n MCP server on the registry today is a wrapper around the same public
11
+ endpoints: list workflows, list executions, create, activate, delete. That is
12
+ useful for building workflows. It is close to useless for keeping a self-hosted
13
+ instance alive, because the things that actually take an instance down are not
14
+ in that API at all.
15
+
16
+ n8n-guard answers the four questions the API cannot:
17
+
18
+ 1. **How big is the datastore, and when does it become a problem?** The Public
19
+ API has no endpoint for database size. n8n-guard reads the SQLite file or
20
+ queries Postgres directly, breaks the size down per table, derives the growth
21
+ rate, and tells you how many days of headroom are left.
22
+ 2. **Which executions are stuck?** Listing executions by status is not
23
+ detection. A run sitting in `running` looks identical to a healthy long job
24
+ until you compare it against what that workflow normally takes. n8n-guard
25
+ builds a per-workflow median from finished runs and flags the outliers.
26
+ 3. **What is retention costing?** How many execution records are stored, how far
27
+ back they reach, and what a given retention window would free.
28
+ 4. **Has the instance drifted from git?** Existing servers diff one API object
29
+ against another. n8n-guard diffs the running instance against the JSON
30
+ exports in a repository: live but never exported, exported but deleted,
31
+ and content that quietly diverged.
32
+
33
+ It is strictly read-only. n8n-guard never writes to your instance, never deletes
34
+ executions, and never activates or edits a workflow. It reports; you decide.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npx -y n8n-guard
40
+ ```
41
+
42
+ Requires Node.js 22 or newer (it uses the built-in `node:sqlite`). For a
43
+ permanent install use `npm install -g n8n-guard`.
44
+
45
+ ### Claude Desktop / any MCP client
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "n8n-guard": {
51
+ "command": "n8n-guard",
52
+ "env": {
53
+ "N8N_URL": "https://n8n.example.com",
54
+ "N8N_API_KEY": "n8n_api_...",
55
+ "N8N_SQLITE_PATH": "/var/lib/n8n/.n8n/database.sqlite",
56
+ "N8N_GIT_REPO_PATH": "/srv/workflow-exports"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Configuration
64
+
65
+ Everything is an environment variable. There are no hardcoded hosts, paths,
66
+ container names or accounts, so the server runs against any self-hosted
67
+ instance.
68
+
69
+ | Variable | Required | Default | Purpose |
70
+ |---|---|---|---|
71
+ | `N8N_URL` | no | `http://localhost:5678` | Base URL of the instance |
72
+ | `N8N_API_KEY` | for execution and workflow checks | none | Public API key (Settings, n8n API) |
73
+ | `N8N_SQLITE_PATH` | for datastore checks on SQLite | none | Path to `database.sqlite` |
74
+ | `N8N_POSTGRES_URL` | for datastore checks on Postgres | none | Connection string, takes precedence over SQLite |
75
+ | `N8N_GIT_REPO_PATH` | for the drift check | none | Checkout holding exported workflow JSON |
76
+ | `N8N_GUARD_STUCK_MINUTES` | no | `15` | Minutes in `running` before a run counts as stuck |
77
+ | `N8N_GUARD_DB_WARN_BYTES` | no | `1073741824` (1 GB) | Datastore warning threshold |
78
+ | `N8N_GUARD_DB_CRITICAL_BYTES` | no | `4294967296` (4 GB) | Datastore critical threshold |
79
+ | `N8N_GUARD_TIMEOUT_MS` | no | `15000` | Per-request API timeout |
80
+
81
+ Partial configuration is a supported mode, not an error. With only `N8N_URL` and
82
+ `N8N_SQLITE_PATH` you still get disk and retention answers; the API-backed checks
83
+ report that they are unconfigured and the rest of the run continues.
84
+
85
+ ## Tools
86
+
87
+ | Tool | What it does |
88
+ |---|---|
89
+ | `instance_info` | Reachability, configured datastore, workflow inventory. Shows which checks are usable. |
90
+ | `db_health` | Datastore size, per-table breakdown, growth rate, projected days until the critical threshold. |
91
+ | `stuck_executions` | Runs stuck in `running`, judged against the absolute threshold and each workflow's own median. |
92
+ | `retention_report` | Execution count, age span, and what pruning to a retention window would free. |
93
+ | `git_drift` | Live workflows against the repository exports: missing, deleted, drifted, duplicated. |
94
+ | `guard_run` | All of the above in one resilient sweep. This is the one to schedule. |
95
+
96
+ ## Example call
97
+
98
+ Point the server at an instance and let your client call a tool. Every tool
99
+ answers with a readable summary first and the full structured payload after it:
100
+
101
+ ```
102
+ ## Retention report
103
+ - 200 execution records spanning 8.3 days (24.1/day)
104
+ - nothing older than 30 days, retention is already tight
105
+ - n8n-guard never deletes. To act on this, set EXECUTIONS_DATA_PRUNE=true and
106
+ EXECUTIONS_DATA_MAX_AGE=720 (hours) on the n8n instance, then restart it.
107
+ ```
108
+
109
+ followed by the structured payload the summary was derived from:
110
+
111
+ ```json
112
+ {
113
+ "executionCount": 200,
114
+ "oldestExecutionAt": "2026-08-28T23:43:28.651Z",
115
+ "spanDays": 8.29,
116
+ "executionsPerDay": 24.12,
117
+ "buckets": [
118
+ { "olderThanDays": 7, "rows": 32, "share": 0.16, "estimatedBytes": 3277 },
119
+ { "olderThanDays": 30, "rows": 0, "share": 0, "estimatedBytes": 0 }
120
+ ]
121
+ }
122
+ ```
123
+
124
+ `guard_run` wraps all six checks. With only a datastore configured it reports
125
+ the gap instead of aborting:
126
+
127
+ ```
128
+ ## Guard run (degraded)
129
+ - stuck_executions failed after 3 attempt(s): N8N_API_KEY is not set, so the n8n Public API cannot be queried
130
+ - git_drift failed after 2 attempt(s): N8N_GIT_REPO_PATH is not set, so there is nothing to diff the instance against
131
+ - 4 of 6 checks completed despite the failures above
132
+ ```
133
+
134
+ ## Recovery: `guard_run` survives a partial outage
135
+
136
+ A monitoring run that aborts on the first unreachable dependency is worthless
137
+ precisely when it matters. The API being down *is* the incident, and it is no
138
+ reason to stop looking at the disk.
139
+
140
+ `guard_run` isolates every step. A failing step is retried with backoff, and if
141
+ it still fails it becomes a finding while the remaining checks run to completion.
142
+ Steps that depend on a failed step are skipped with the reason attached rather
143
+ than crashing on missing input. The report says so plainly:
144
+
145
+ ```
146
+ ## Guard run (degraded)
147
+ - stuck_executions failed after 3 attempt(s): cannot reach http://n8n.internal:5678: fetch failed
148
+ - git_drift skipped: depends on instance, which failed
149
+ - 4 of 6 checks completed despite the failures above
150
+ ```
151
+
152
+ This behaviour is covered by tests in `test/unit/runner.test.ts` and
153
+ `test/unit/serviceRecovery.test.ts`, the latter running a real SQLite datastore
154
+ against a deliberately dead API endpoint.
155
+
156
+ ## Try it on a throwaway instance
157
+
158
+ The compose file starts a fresh n8n you can safely point the server at. The data
159
+ directory is bind-mounted, which is exactly how n8n-guard reads a real
160
+ deployment.
161
+
162
+ ```bash
163
+ git clone https://github.com/flow-84/n8n-guard && cd n8n-guard
164
+ npm install && npm run build
165
+
166
+ docker compose up -d
167
+ ./scripts/demo-setup.sh # owner, API key, workflows, and a drifted export folder
168
+ ```
169
+
170
+ The script generates the owner password, keeps it in the gitignored
171
+ `.n8n-demo-credentials`, and prints it along with the environment block to run
172
+ the server with. It seeds one
173
+ workflow that matches its export, one that was never exported, and one export
174
+ whose workflow does not exist, so `git_drift` has something real to find.
175
+
176
+ ```bash
177
+ export N8N_URL=http://localhost:5678
178
+ export N8N_API_KEY=... # printed by the script
179
+ export N8N_SQLITE_PATH=$PWD/.n8n-demo/database.sqlite
180
+ export N8N_GIT_REPO_PATH=$PWD/.n8n-demo-exports
181
+ npm start
182
+ ```
183
+
184
+ Use `N8N_DEMO_PORT=5679 docker compose up -d` if port 5678 is already taken by
185
+ an instance you care about.
186
+
187
+ ## Tests
188
+
189
+ ```bash
190
+ npm test # unit tests, no instance required
191
+ npm run test:integration # against the compose instance above
192
+ ```
193
+
194
+ The integration suite skips the API-backed cases when `N8N_API_KEY` is unset, so
195
+ it stays runnable on a bare `docker compose up`.
196
+
197
+ ## License
198
+
199
+ MIT
@@ -0,0 +1,36 @@
1
+ import type { DatastoreStats } from "../n8n/datastore.js";
2
+ export type Severity = "ok" | "warning" | "critical";
3
+ export interface DbHealthThresholds {
4
+ warnBytes: number;
5
+ criticalBytes: number;
6
+ }
7
+ export interface DbHealthReport {
8
+ kind: DatastoreStats["kind"];
9
+ totalBytes: number;
10
+ totalHuman: string;
11
+ severity: Severity;
12
+ components: {
13
+ name: string;
14
+ bytes: number;
15
+ }[];
16
+ tables: {
17
+ table: string;
18
+ rows: number;
19
+ bytes: number | null;
20
+ share: number | null;
21
+ }[];
22
+ /** Average bytes added per day, derived from the age of the oldest execution. */
23
+ growthBytesPerDay: number | null;
24
+ /** Days until the datastore reaches `criticalBytes` at the observed growth rate. */
25
+ daysUntilCritical: number | null;
26
+ /** Bytes stored per execution record; the lever that retention pruning pulls on. */
27
+ bytesPerExecution: number | null;
28
+ findings: string[];
29
+ }
30
+ export declare function humanBytes(bytes: number): string;
31
+ /**
32
+ * Pure assessment of a datastore snapshot. The n8n Public API exposes none of
33
+ * this: it has no endpoint for database size, so an API wrapper cannot warn
34
+ * before the disk fills up and the instance stops accepting executions.
35
+ */
36
+ export declare function assessDatabaseHealth(stats: DatastoreStats, thresholds: DbHealthThresholds, now?: number): DbHealthReport;
@@ -0,0 +1,69 @@
1
+ export function humanBytes(bytes) {
2
+ const units = ["B", "KB", "MB", "GB", "TB"];
3
+ let value = Math.abs(bytes);
4
+ let unit = 0;
5
+ while (value >= 1024 && unit < units.length - 1) {
6
+ value /= 1024;
7
+ unit += 1;
8
+ }
9
+ const sign = bytes < 0 ? "-" : "";
10
+ return `${sign}${value.toFixed(value >= 100 || unit === 0 ? 0 : 1)} ${units[unit]}`;
11
+ }
12
+ /**
13
+ * Pure assessment of a datastore snapshot. The n8n Public API exposes none of
14
+ * this: it has no endpoint for database size, so an API wrapper cannot warn
15
+ * before the disk fills up and the instance stops accepting executions.
16
+ */
17
+ export function assessDatabaseHealth(stats, thresholds, now = Date.now()) {
18
+ const findings = [];
19
+ let severity = "ok";
20
+ if (stats.totalBytes >= thresholds.criticalBytes)
21
+ severity = "critical";
22
+ else if (stats.totalBytes >= thresholds.warnBytes)
23
+ severity = "warning";
24
+ if (severity !== "ok") {
25
+ findings.push(`datastore is ${humanBytes(stats.totalBytes)}, at or past the ${severity} threshold of ` +
26
+ `${humanBytes(severity === "critical" ? thresholds.criticalBytes : thresholds.warnBytes)}`);
27
+ }
28
+ const oldest = stats.oldestExecutionAt ? Date.parse(stats.oldestExecutionAt) : NaN;
29
+ const ageDays = Number.isNaN(oldest) ? null : Math.max((now - oldest) / 86_400_000, 1 / 24);
30
+ const growthBytesPerDay = ageDays === null ? null : stats.totalBytes / ageDays;
31
+ let daysUntilCritical = null;
32
+ if (growthBytesPerDay !== null && growthBytesPerDay > 0) {
33
+ const headroom = thresholds.criticalBytes - stats.totalBytes;
34
+ daysUntilCritical = headroom <= 0 ? 0 : headroom / growthBytesPerDay;
35
+ if (daysUntilCritical <= 30) {
36
+ findings.push(`at ${humanBytes(growthBytesPerDay)}/day the datastore reaches ` +
37
+ `${humanBytes(thresholds.criticalBytes)} in about ${daysUntilCritical.toFixed(1)} days`);
38
+ if (severity === "ok")
39
+ severity = "warning";
40
+ }
41
+ }
42
+ const bytesPerExecution = stats.executionCount > 0 ? stats.totalBytes / stats.executionCount : null;
43
+ const tables = stats.tables
44
+ .map((t) => ({ ...t, share: t.bytes !== null && stats.totalBytes > 0 ? t.bytes / stats.totalBytes : null }))
45
+ .sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0) || b.rows - a.rows);
46
+ const dominant = tables.find((t) => t.share !== null && t.share >= 0.5);
47
+ if (dominant) {
48
+ findings.push(`${dominant.table} holds ${(dominant.share * 100).toFixed(0)}% of the datastore`);
49
+ }
50
+ if (stats.kind === "sqlite" && tables.every((t) => t.bytes === null)) {
51
+ findings.push("per-table sizes unavailable: this SQLite build lacks the dbstat module, row counts only");
52
+ }
53
+ if (findings.length === 0) {
54
+ findings.push(`datastore is ${humanBytes(stats.totalBytes)} and below all thresholds`);
55
+ }
56
+ return {
57
+ kind: stats.kind,
58
+ totalBytes: stats.totalBytes,
59
+ totalHuman: humanBytes(stats.totalBytes),
60
+ severity,
61
+ components: stats.components,
62
+ tables,
63
+ growthBytesPerDay,
64
+ daysUntilCritical,
65
+ bytesPerExecution,
66
+ findings,
67
+ };
68
+ }
69
+ //# sourceMappingURL=dbHealth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dbHealth.js","sourceRoot":"","sources":["../../src/checks/dbHealth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { N8nWorkflow } from "../n8n/api.js";
2
+ export interface ExportedWorkflow {
3
+ id: string | null;
4
+ name: string;
5
+ nodes?: unknown[];
6
+ connections?: Record<string, unknown>;
7
+ /** Repo-relative path of the file this was read from. */
8
+ file: string;
9
+ }
10
+ export type DriftKind = "missing_in_git" | "missing_in_instance" | "content_drift" | "duplicate_export";
11
+ export interface DriftEntry {
12
+ kind: DriftKind;
13
+ workflowId: string | null;
14
+ name: string;
15
+ file: string | null;
16
+ active: boolean | null;
17
+ detail: string;
18
+ }
19
+ export interface GitDriftReport {
20
+ liveCount: number;
21
+ exportedCount: number;
22
+ inSync: number;
23
+ drift: DriftEntry[];
24
+ findings: string[];
25
+ }
26
+ /**
27
+ * Fingerprint of what a workflow *does*: node types, their parameters and the
28
+ * wiring. Canvas positions, timestamps and per-instance ids are excluded, so a
29
+ * dragged node is not reported as drift.
30
+ */
31
+ export declare function fingerprintWorkflow(workflow: {
32
+ nodes?: unknown[];
33
+ connections?: unknown;
34
+ }): string;
35
+ /**
36
+ * The diff every existing n8n MCP server is missing: it compares two API
37
+ * objects, not the running instance against what is versioned in git.
38
+ */
39
+ export declare function diffWorkflows(live: N8nWorkflow[], exported: ExportedWorkflow[]): GitDriftReport;
40
+ /** Reads every workflow-shaped JSON file under `repoPath`, recursively. */
41
+ export declare function loadExportedWorkflows(repoPath: string): Promise<ExportedWorkflow[]>;
@@ -0,0 +1,193 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ /**
5
+ * Fingerprint of what a workflow *does*: node types, their parameters and the
6
+ * wiring. Canvas positions, timestamps and per-instance ids are excluded, so a
7
+ * dragged node is not reported as drift.
8
+ */
9
+ export function fingerprintWorkflow(workflow) {
10
+ const nodes = Array.isArray(workflow.nodes) ? workflow.nodes : [];
11
+ const normalizedNodes = nodes
12
+ .map((raw) => {
13
+ const node = (raw ?? {});
14
+ return {
15
+ name: node.name ?? null,
16
+ type: node.type ?? null,
17
+ typeVersion: node.typeVersion ?? null,
18
+ disabled: node.disabled ?? false,
19
+ parameters: node.parameters ?? {},
20
+ };
21
+ })
22
+ .sort((a, b) => String(a.name).localeCompare(String(b.name)));
23
+ return createHash("sha256")
24
+ .update(stableStringify({ nodes: normalizedNodes, connections: workflow.connections ?? {} }))
25
+ .digest("hex");
26
+ }
27
+ function stableStringify(value) {
28
+ if (value === null || typeof value !== "object")
29
+ return JSON.stringify(value) ?? "null";
30
+ if (Array.isArray(value))
31
+ return `[${value.map(stableStringify).join(",")}]`;
32
+ const entries = Object.entries(value)
33
+ .filter(([, v]) => v !== undefined)
34
+ .sort(([a], [b]) => a.localeCompare(b));
35
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
36
+ }
37
+ /**
38
+ * The diff every existing n8n MCP server is missing: it compares two API
39
+ * objects, not the running instance against what is versioned in git.
40
+ */
41
+ export function diffWorkflows(live, exported) {
42
+ const available = new Set(exported);
43
+ const claimed = new Map();
44
+ const pick = (predicate) => {
45
+ for (const item of available)
46
+ if (predicate(item))
47
+ return item;
48
+ return undefined;
49
+ };
50
+ // Two passes, because an export may only be claimed once. Duplicating a
51
+ // workflow in n8n keeps the name, so same-name pairs are the normal case:
52
+ // matching by name first would let one of them consume the export another
53
+ // workflow owns by id, and the unexported twin would silently look in sync.
54
+ const assignment = new Map();
55
+ for (const workflow of live) {
56
+ const match = pick((item) => item.id !== null && item.id === String(workflow.id));
57
+ if (match) {
58
+ available.delete(match);
59
+ claimed.set(match, workflow);
60
+ assignment.set(workflow, match);
61
+ }
62
+ }
63
+ for (const workflow of live) {
64
+ if (assignment.has(workflow))
65
+ continue;
66
+ const match = pick((item) => item.name === workflow.name);
67
+ if (match) {
68
+ available.delete(match);
69
+ claimed.set(match, workflow);
70
+ assignment.set(workflow, match);
71
+ }
72
+ }
73
+ const drift = [];
74
+ let inSync = 0;
75
+ for (const workflow of live) {
76
+ const match = assignment.get(workflow);
77
+ if (!match) {
78
+ drift.push({
79
+ kind: "missing_in_git",
80
+ workflowId: String(workflow.id),
81
+ name: workflow.name,
82
+ file: null,
83
+ active: workflow.active,
84
+ detail: workflow.active
85
+ ? "active in the instance but not exported to the repository"
86
+ : "present in the instance but not exported to the repository",
87
+ });
88
+ continue;
89
+ }
90
+ if (fingerprintWorkflow(workflow) === fingerprintWorkflow(match)) {
91
+ inSync += 1;
92
+ }
93
+ else {
94
+ drift.push({
95
+ kind: "content_drift",
96
+ workflowId: String(workflow.id),
97
+ name: workflow.name,
98
+ file: match.file,
99
+ active: workflow.active,
100
+ detail: `nodes or connections differ between the instance and ${match.file}`,
101
+ });
102
+ }
103
+ }
104
+ // Whatever is left over was claimed by nobody. If a live workflow already
105
+ // took an export with the same identity, the leftovers are redundant copies
106
+ // of it; otherwise the workflow they describe is gone from the instance.
107
+ const identities = new Map();
108
+ for (const [item, workflow] of claimed) {
109
+ if (item.id)
110
+ identities.set(`id:${item.id}`, workflow);
111
+ identities.set(`name:${item.name}`, workflow);
112
+ }
113
+ for (const item of available) {
114
+ const owner = (item.id ? identities.get(`id:${item.id}`) : undefined) ?? identities.get(`name:${item.name}`);
115
+ if (owner) {
116
+ drift.push({
117
+ kind: "duplicate_export",
118
+ workflowId: String(owner.id),
119
+ name: owner.name,
120
+ file: item.file,
121
+ active: owner.active,
122
+ detail: `a second export file claims this workflow, alongside ${assignment.get(owner).file}`,
123
+ });
124
+ continue;
125
+ }
126
+ drift.push({
127
+ kind: "missing_in_instance",
128
+ workflowId: item.id,
129
+ name: item.name,
130
+ file: item.file,
131
+ active: null,
132
+ detail: "exported in the repository but not present in the instance (deleted or never imported)",
133
+ });
134
+ }
135
+ const counts = drift.reduce((acc, entry) => {
136
+ acc[entry.kind] = (acc[entry.kind] ?? 0) + 1;
137
+ return acc;
138
+ }, {});
139
+ const findings = drift.length === 0
140
+ ? [`all ${live.length} workflows match their exports`]
141
+ : [
142
+ `${drift.length} differences across ${live.length} live and ${exported.length} exported workflows`,
143
+ ...Object.entries(counts).map(([kind, n]) => `${kind}: ${n}`),
144
+ ];
145
+ return { liveCount: live.length, exportedCount: exported.length, inSync, drift, findings };
146
+ }
147
+ const SKIP_DIRS = new Set([".git", "node_modules", ".github", "dist", "build"]);
148
+ /** Reads every workflow-shaped JSON file under `repoPath`, recursively. */
149
+ export async function loadExportedWorkflows(repoPath) {
150
+ const root = path.resolve(repoPath);
151
+ const info = await stat(root).catch(() => null);
152
+ if (!info?.isDirectory()) {
153
+ throw new Error(`N8N_GIT_REPO_PATH is not a readable directory: ${repoPath}`);
154
+ }
155
+ const found = [];
156
+ const walk = async (dir) => {
157
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
158
+ for (const entry of entries) {
159
+ const full = path.join(dir, entry.name);
160
+ if (entry.isDirectory()) {
161
+ if (!SKIP_DIRS.has(entry.name))
162
+ await walk(full);
163
+ continue;
164
+ }
165
+ if (!entry.name.endsWith(".json"))
166
+ continue;
167
+ const parsed = await readFile(full, "utf8").then((text) => JSON.parse(text)).catch(() => null);
168
+ for (const candidate of Array.isArray(parsed) ? parsed : [parsed]) {
169
+ const workflow = toExported(candidate, path.relative(root, full));
170
+ if (workflow)
171
+ found.push(workflow);
172
+ }
173
+ }
174
+ };
175
+ await walk(root);
176
+ return found;
177
+ }
178
+ function toExported(value, file) {
179
+ if (!value || typeof value !== "object")
180
+ return null;
181
+ const record = value;
182
+ // A workflow export always carries nodes; package.json and friends do not.
183
+ if (!Array.isArray(record.nodes))
184
+ return null;
185
+ return {
186
+ id: record.id === undefined || record.id === null ? null : String(record.id),
187
+ name: typeof record.name === "string" ? record.name : path.basename(file, ".json"),
188
+ nodes: record.nodes,
189
+ connections: (record.connections ?? {}),
190
+ file,
191
+ };
192
+ }
193
+ //# sourceMappingURL=gitDrift.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitDrift.js","sourceRoot":"","sources":["../../src/checks/gitDrift.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,IAAI,MAAM,WAAW,CAAC;AA+B7B;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAsD;IACxF,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,MAAM,eAAe,GAAG,KAAK;SAC1B,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;QACX,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,CAA4B,CAAC;QACpD,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;YACvB,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,IAAI;YACvB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;YACrC,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,KAAK;YAChC,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,EAAE;SAClC,CAAC;IACJ,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAEhE,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC,CAAC;SAC5F,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED,SAAS,eAAe,CAAC,KAAc;IACrC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;IACxF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC7E,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;SAC7D,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC;SAClC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAChG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAmB,EAAE,QAA4B;IAC7E,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAiC,CAAC;IAEzD,MAAM,IAAI,GAAG,CAAC,SAA8C,EAAgC,EAAE;QAC5F,KAAK,MAAM,IAAI,IAAI,SAAS;YAAE,IAAI,SAAS,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;QAC/D,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,wEAAwE;IACxE,0EAA0E;IAC1E,0EAA0E;IAC1E,4EAA4E;IAC5E,MAAM,UAAU,GAAG,IAAI,GAAG,EAAiC,CAAC;IAC5D,KAAK,MAAM,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,IAAI,IAAI,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;QAClF,IAAI,KAAK,EAAE,CAAC;YACV,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC7B,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5B,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC;YAAE,SAAS;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,KAAK,EAAE,CAAC;YACV,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAC7B,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACvC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,gBAAgB;gBACtB,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/B,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,IAAI,EAAE,IAAI;gBACV,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,MAAM,EAAE,QAAQ,CAAC,MAAM;oBACrB,CAAC,CAAC,2DAA2D;oBAC7D,CAAC,CAAC,4DAA4D;aACjE,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,IAAI,mBAAmB,CAAC,QAAQ,CAAC,KAAK,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;YACjE,MAAM,IAAI,CAAC,CAAC;QACd,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,eAAe;gBACrB,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC/B,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,MAAM,EAAE,wDAAwD,KAAK,CAAC,IAAI,EAAE;aAC7E,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,4EAA4E;IAC5E,yEAAyE;IACzE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;IAClD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,OAAO,EAAE,CAAC;QACvC,IAAI,IAAI,CAAC,EAAE;YAAE,UAAU,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;QACvD,UAAU,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;IAChD,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7G,IAAI,KAAK,EAAE,CAAC;YACV,KAAK,CAAC,IAAI,CAAC;gBACT,IAAI,EAAE,kBAAkB;gBACxB,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,MAAM,EAAE,wDAAwD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAE,CAAC,IAAI,EAAE;aAC9F,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,qBAAqB;YAC3B,UAAU,EAAE,IAAI,CAAC,EAAE;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI;YACZ,MAAM,EAAE,wFAAwF;SACjG,CAAC,CAAC;IACL,CAAC;IAED,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAyB,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;QACjE,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7C,OAAO,GAAG,CAAC;IACb,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,QAAQ,GACZ,KAAK,CAAC,MAAM,KAAK,CAAC;QAChB,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,MAAM,gCAAgC,CAAC;QACtD,CAAC,CAAC;YACE,GAAG,KAAK,CAAC,MAAM,uBAAuB,IAAI,CAAC,MAAM,aAAa,QAAQ,CAAC,MAAM,qBAAqB;YAClG,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,CAAC,EAAE,CAAC;SAC9D,CAAC;IAER,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;AAC7F,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEhF,2EAA2E;AAC3E,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,QAAgB;IAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,kDAAkD,QAAQ,EAAE,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,KAAK,GAAuB,EAAE,CAAC;IACrC,MAAM,IAAI,GAAG,KAAK,EAAE,GAAW,EAAiB,EAAE;QAChD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5E,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;gBACjD,SAAS;YACX,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;gBAAE,SAAS;YAC5C,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAY,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1G,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClE,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;gBAClE,IAAI,QAAQ;oBAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CAAC,KAAc,EAAE,IAAY;IAC9C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,2EAA2E;IAC3E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC9C,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,EAAE,KAAK,SAAS,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC5E,IAAI,EAAE,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAClF,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,WAAW,EAAE,CAAC,MAAM,CAAC,WAAW,IAAI,EAAE,CAA4B;QAClE,IAAI;KACL,CAAC;AACJ,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { DatastoreStats } from "../n8n/datastore.js";
2
+ export interface RetentionReport {
3
+ executionCount: number;
4
+ oldestExecutionAt: string | null;
5
+ newestExecutionAt: string | null;
6
+ spanDays: number | null;
7
+ executionsPerDay: number | null;
8
+ buckets: {
9
+ olderThanDays: number;
10
+ rows: number;
11
+ share: number;
12
+ estimatedBytes: number | null;
13
+ }[];
14
+ recommendation: {
15
+ keepDays: number;
16
+ prunableRows: number;
17
+ estimatedBytesFreed: number | null;
18
+ estimatedHumanFreed: string | null;
19
+ } | null;
20
+ findings: string[];
21
+ /** n8n prunes on its own only when these are set; guard reports, it never writes. */
22
+ hint: string;
23
+ }
24
+ /**
25
+ * Turns raw execution counts into "what does deleting old runs actually buy me".
26
+ * Byte figures are estimates: they assume execution rows cost the same on
27
+ * average, which is what the row-count-to-size ratio can support.
28
+ */
29
+ export declare function analyzeRetention(stats: DatastoreStats, keepDays?: number, now?: number): RetentionReport;