paybridge 0.9.0 → 0.11.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/README.md +80 -0
- package/dist/cli/commands/drift-watch.d.ts +1 -0
- package/dist/cli/commands/drift-watch.js +118 -0
- package/dist/cli/commands/drift.d.ts +20 -0
- package/dist/cli/commands/drift.js +212 -0
- package/dist/cli/commands/reconcile.d.ts +1 -0
- package/dist/cli/commands/reconcile.js +341 -0
- package/dist/cli/drift-store.d.ts +14 -0
- package/dist/cli/drift-store.js +80 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/reconcile-types.d.ts +22 -0
- package/dist/cli/reconcile-types.js +2 -0
- package/dist/cli/reconcile.d.ts +12 -0
- package/dist/cli/reconcile.js +102 -0
- package/dist/cli/runners.js +49 -11
- package/dist/cli/utils.js +17 -0
- package/dist/drift-detector.d.ts +40 -0
- package/dist/drift-detector.js +103 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/providers/square.js +1 -1
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -61,6 +61,86 @@ Runnable integrations for common Node.js frameworks:
|
|
|
61
61
|
|
|
62
62
|
Each example uses `PayBridgeRouter` with Stripe + PayStack and demonstrates webhook signature verification, idempotency, and provider-specific routing.
|
|
63
63
|
|
|
64
|
+
## Drift Detection
|
|
65
|
+
|
|
66
|
+
Payment providers change their APIs without notice. A field gets renamed, an endpoint moves, a type changes from `string` to `number`. Your integration silently breaks.
|
|
67
|
+
|
|
68
|
+
PayBridge includes **drift detection** — capture the shape of every provider's sandbox response, store it as a baseline, and get alerted the moment something changes.
|
|
69
|
+
|
|
70
|
+
### Quick Start
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Capture baselines (one-time setup)
|
|
74
|
+
npx paybridge drift-check --capture
|
|
75
|
+
|
|
76
|
+
# Check for drift
|
|
77
|
+
npx paybridge drift-check
|
|
78
|
+
|
|
79
|
+
# Watch continuously (6-hour interval)
|
|
80
|
+
npx paybridge drift-watch --interval 6h --webhook-url https://hooks.slack.com/...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Example Output
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
=== Drift Detection ===
|
|
87
|
+
|
|
88
|
+
[✓] stripe — no drift
|
|
89
|
+
[⚠] mollie — drift detected:
|
|
90
|
+
+ new keys: data.expiresAt, _links.dashboard.href
|
|
91
|
+
- removed keys: data.metadata.legacy
|
|
92
|
+
! type changed: data.amount.value (string → number)
|
|
93
|
+
[⚠] square — drift detected:
|
|
94
|
+
+ new keys: payment_link.created_at_iso
|
|
95
|
+
[ ] paystack — Missing: PAYSTACK_API_KEY
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Exit code 1 if drift detected, 0 if clean. Perfect for CI/CD pipelines or cron jobs.
|
|
99
|
+
|
|
100
|
+
### Why It Matters
|
|
101
|
+
|
|
102
|
+
The Square `/checkout/payment-links → /online-checkout/payment-links` endpoint change would have shipped silently to production. With `drift-check` running daily, you get a Slack alert the moment it happens.
|
|
103
|
+
|
|
104
|
+
## Reconciliation
|
|
105
|
+
|
|
106
|
+
Webhooks can fail. Networks blip. Your server hiccups. Provider retries don't reach you. Without reconciliation, you discover missed webhooks when a customer complains their account wasn't credited.
|
|
107
|
+
|
|
108
|
+
PayBridge's **reconcile** command diffs your database against each provider's current state, catching payments where your local status doesn't match reality.
|
|
109
|
+
|
|
110
|
+
### Quick Start
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# From JSONL file
|
|
114
|
+
echo '{"provider":"stripe","reference":"pay_001","expectedStatus":"pending"}' > expected.jsonl
|
|
115
|
+
npx paybridge reconcile --input expected.jsonl
|
|
116
|
+
|
|
117
|
+
# From SQL query (Postgres example)
|
|
118
|
+
psql -t -c "SELECT provider, reference, status AS \"expectedStatus\" FROM payments WHERE status='pending' AND created_at > now() - interval '24 hours'" \
|
|
119
|
+
| npx paybridge reconcile
|
|
120
|
+
|
|
121
|
+
# CSV format
|
|
122
|
+
cat payments.csv | npx paybridge reconcile
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Example Output
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
[✓] stripe:pay_001 — completed (match)
|
|
129
|
+
[!] stripe:pay_002 — expected pending, actual completed (MISSED WEBHOOK)
|
|
130
|
+
[?] paystack:pay_003 — not-found (no provider record)
|
|
131
|
+
[✗] stripe:pay_004 — error (HTTP 503)
|
|
132
|
+
[ ] adyen:pay_005 — skipped (missing ADYEN_API_KEY)
|
|
133
|
+
|
|
134
|
+
Reconciled: 5
|
|
135
|
+
Match: 1
|
|
136
|
+
Mismatch (missed webhook): 1
|
|
137
|
+
Not found: 1
|
|
138
|
+
Error: 1
|
|
139
|
+
Skipped: 1
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Exit code 1 if any mismatch, 0 if clean. Add `--webhook-url` to POST mismatch reports to Slack/Discord/your ops channel. Use `--json` for pipeline integration.
|
|
143
|
+
|
|
64
144
|
## Quick Start
|
|
65
145
|
|
|
66
146
|
> **Upgrading from 0.1 or 0.2?** See [docs/migration.md](docs/migration.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDriftWatch(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runDriftWatch = runDriftWatch;
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const runners_1 = require("../runners");
|
|
39
|
+
const drift_store_1 = require("../drift-store");
|
|
40
|
+
const drift_1 = require("./drift");
|
|
41
|
+
const utils_1 = require("../utils");
|
|
42
|
+
const INTERVALS = {
|
|
43
|
+
'30m': 30 * 60 * 1000,
|
|
44
|
+
'1h': 60 * 60 * 1000,
|
|
45
|
+
'6h': 6 * 60 * 60 * 1000,
|
|
46
|
+
'12h': 12 * 60 * 60 * 1000,
|
|
47
|
+
'24h': 24 * 60 * 60 * 1000,
|
|
48
|
+
};
|
|
49
|
+
function parseInterval(str) {
|
|
50
|
+
const interval = INTERVALS[str];
|
|
51
|
+
if (!interval) {
|
|
52
|
+
throw new Error(`Invalid interval: ${str}. Supported: ${Object.keys(INTERVALS).join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
return interval;
|
|
55
|
+
}
|
|
56
|
+
async function runDriftWatch(args) {
|
|
57
|
+
const intervalArg = args.find((a, i) => args[i - 1] === '--interval') || '6h';
|
|
58
|
+
const interval = parseInterval(intervalArg);
|
|
59
|
+
const once = args.includes('--once');
|
|
60
|
+
const webhookUrl = args.find((a, i) => args[i - 1] === '--webhook-url');
|
|
61
|
+
const baselineDir = args.find((a, i) => args[i - 1] === '--baseline-dir') ||
|
|
62
|
+
path.join(process.cwd(), '.paybridge', 'drift-baseline');
|
|
63
|
+
const store = new drift_store_1.FileDriftStore(baselineDir);
|
|
64
|
+
let running = true;
|
|
65
|
+
const cleanup = () => {
|
|
66
|
+
running = false;
|
|
67
|
+
console.log('\nStopping drift watch...');
|
|
68
|
+
process.exit(0);
|
|
69
|
+
};
|
|
70
|
+
process.on('SIGTERM', cleanup);
|
|
71
|
+
process.on('SIGINT', cleanup);
|
|
72
|
+
const runCheck = async () => {
|
|
73
|
+
const timestamp = new Date().toISOString();
|
|
74
|
+
console.log(`\n${(0, utils_1.colorize)('[drift-watch]', 'cyan')} Running check at ${timestamp}\n`);
|
|
75
|
+
try {
|
|
76
|
+
const results = await (0, drift_1.runDriftCheck)(runners_1.runners, store, { webhookUrl });
|
|
77
|
+
const driftCount = results.filter((r) => r.status === 'drift').length;
|
|
78
|
+
const noDriftCount = results.filter((r) => r.status === 'no-drift').length;
|
|
79
|
+
const skippedCount = results.filter((r) => r.status === 'skipped').length;
|
|
80
|
+
const errorCount = results.filter((r) => r.status === 'error').length;
|
|
81
|
+
for (const res of results) {
|
|
82
|
+
if (res.status === 'drift' && res.report) {
|
|
83
|
+
console.log(`${(0, utils_1.colorize)('[⚠]', 'yellow')} ${res.provider} — drift detected`);
|
|
84
|
+
if (res.report.addedKeys.length > 0) {
|
|
85
|
+
console.log(` + new keys: ${res.report.addedKeys.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
if (res.report.removedKeys.length > 0) {
|
|
88
|
+
console.log(` - removed keys: ${res.report.removedKeys.join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
if (res.report.typeChanges.length > 0) {
|
|
91
|
+
for (const change of res.report.typeChanges) {
|
|
92
|
+
console.log(` ! type changed: ${change.key} (${change.oldType} → ${change.newType})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (res.status === 'error') {
|
|
97
|
+
console.log(`${(0, utils_1.colorize)('[✗]', 'red')} ${res.provider} ERROR: ${res.message}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log(`\n${(0, utils_1.colorize)('[summary]', 'cyan')} drift: ${driftCount}, clean: ${noDriftCount}, skipped: ${skippedCount}, errors: ${errorCount}`);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.error(`${(0, utils_1.colorize)('[!]', 'red')} Check failed: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
await runCheck();
|
|
107
|
+
if (once) {
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
console.log(`\n${(0, utils_1.colorize)('[drift-watch]', 'cyan')} Watching every ${intervalArg}. Press Ctrl+C to stop.\n`);
|
|
111
|
+
const timer = setInterval(() => {
|
|
112
|
+
if (running) {
|
|
113
|
+
runCheck();
|
|
114
|
+
}
|
|
115
|
+
}, interval);
|
|
116
|
+
timer.unref();
|
|
117
|
+
await new Promise(() => { });
|
|
118
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ProviderRunner } from '../runners';
|
|
2
|
+
import { DriftStore } from '../drift-store';
|
|
3
|
+
import { DriftReport } from '../../drift-detector';
|
|
4
|
+
interface DriftCheckOptions {
|
|
5
|
+
capture?: boolean;
|
|
6
|
+
baselineDir?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
webhookUrl?: string;
|
|
9
|
+
providers?: string[];
|
|
10
|
+
}
|
|
11
|
+
interface DriftCheckResult {
|
|
12
|
+
provider: string;
|
|
13
|
+
status: 'captured' | 'no-baseline' | 'no-drift' | 'drift' | 'skipped' | 'error';
|
|
14
|
+
message?: string;
|
|
15
|
+
report?: DriftReport;
|
|
16
|
+
keyCount?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function runDriftCheck(providedRunners: ProviderRunner[], store: DriftStore, opts: DriftCheckOptions): Promise<DriftCheckResult[]>;
|
|
19
|
+
export declare function runDrift(args: string[]): Promise<void>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runDriftCheck = runDriftCheck;
|
|
37
|
+
exports.runDrift = runDrift;
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const runners_1 = require("../runners");
|
|
40
|
+
const drift_store_1 = require("../drift-store");
|
|
41
|
+
const drift_detector_1 = require("../../drift-detector");
|
|
42
|
+
const utils_1 = require("../utils");
|
|
43
|
+
async function postWebhook(url, payload) {
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(payload),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(`Webhook POST failed: ${res.status} ${res.statusText}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function runDriftCheck(providedRunners, store, opts) {
|
|
54
|
+
const libVersion = '0.10.0';
|
|
55
|
+
const results = [];
|
|
56
|
+
for (const runner of providedRunners) {
|
|
57
|
+
const missing = runner.envRequired.filter((key) => !process.env[key]);
|
|
58
|
+
if (missing.length > 0) {
|
|
59
|
+
results.push({
|
|
60
|
+
provider: runner.name,
|
|
61
|
+
status: 'skipped',
|
|
62
|
+
message: `Missing: ${missing.join(', ')}`,
|
|
63
|
+
});
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const response = await runner.run();
|
|
68
|
+
const shape = (0, drift_detector_1.captureShape)(response);
|
|
69
|
+
shape.status = response.status;
|
|
70
|
+
if (opts.capture) {
|
|
71
|
+
const baseline = {
|
|
72
|
+
providerName: runner.name,
|
|
73
|
+
operation: 'createPayment',
|
|
74
|
+
shape,
|
|
75
|
+
libVersion,
|
|
76
|
+
};
|
|
77
|
+
await store.save(baseline);
|
|
78
|
+
results.push({
|
|
79
|
+
provider: runner.name,
|
|
80
|
+
status: 'captured',
|
|
81
|
+
keyCount: shape.keys.length,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const baseline = await store.load(runner.name);
|
|
86
|
+
if (!baseline) {
|
|
87
|
+
results.push({
|
|
88
|
+
provider: runner.name,
|
|
89
|
+
status: 'no-baseline',
|
|
90
|
+
message: 'No baseline found (run with --capture to create)',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const report = (0, drift_detector_1.diffBaseline)(baseline, shape, runner.name);
|
|
95
|
+
if (report.driftDetected) {
|
|
96
|
+
results.push({
|
|
97
|
+
provider: runner.name,
|
|
98
|
+
status: 'drift',
|
|
99
|
+
report,
|
|
100
|
+
});
|
|
101
|
+
if (opts.webhookUrl) {
|
|
102
|
+
try {
|
|
103
|
+
await postWebhook(opts.webhookUrl, { provider: runner.name, drift: report, libVersion });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`${(0, utils_1.colorize)('[!]', 'yellow')} ${runner.name} webhook failed: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
results.push({
|
|
112
|
+
provider: runner.name,
|
|
113
|
+
status: 'no-drift',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
results.push({
|
|
121
|
+
provider: runner.name,
|
|
122
|
+
status: 'error',
|
|
123
|
+
message: error.message || String(error),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
async function runDrift(args) {
|
|
130
|
+
const capture = args.includes('--capture');
|
|
131
|
+
const jsonOutput = args.includes('--json');
|
|
132
|
+
const baselineDir = args.find((a, i) => args[i - 1] === '--baseline-dir') ||
|
|
133
|
+
path.join(process.cwd(), '.paybridge', 'drift-baseline');
|
|
134
|
+
const webhookUrl = args.find((a, i) => args[i - 1] === '--webhook-url');
|
|
135
|
+
const providerNames = args.filter((a) => !a.startsWith('--') && a !== 'drift-check');
|
|
136
|
+
let selectedRunners = runners_1.runners;
|
|
137
|
+
if (providerNames.length > 0) {
|
|
138
|
+
selectedRunners = runners_1.runners.filter((r) => providerNames.includes(r.name));
|
|
139
|
+
const unknownProviders = providerNames.filter((p) => !runners_1.runners.find((r) => r.name === p));
|
|
140
|
+
if (unknownProviders.length > 0) {
|
|
141
|
+
console.error(`Unknown providers: ${unknownProviders.join(', ')}`);
|
|
142
|
+
console.error(`Available: ${runners_1.runners.map((r) => r.name).join(', ')}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const store = new drift_store_1.FileDriftStore(baselineDir);
|
|
147
|
+
const results = await runDriftCheck(selectedRunners, store, { capture, baselineDir, json: jsonOutput, webhookUrl });
|
|
148
|
+
if (jsonOutput) {
|
|
149
|
+
console.log(JSON.stringify(results, null, 2));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
if (capture) {
|
|
153
|
+
console.log('\n=== Drift Baseline Capture ===\n');
|
|
154
|
+
for (const res of results) {
|
|
155
|
+
if (res.status === 'captured') {
|
|
156
|
+
console.log(`${(0, utils_1.colorize)('[saved]', 'green')} ${res.provider} baseline (${res.keyCount} keys)`);
|
|
157
|
+
}
|
|
158
|
+
else if (res.status === 'skipped') {
|
|
159
|
+
console.log(`${(0, utils_1.colorize)('[ ]', 'dim')} ${res.provider} — ${res.message}`);
|
|
160
|
+
}
|
|
161
|
+
else if (res.status === 'error') {
|
|
162
|
+
console.log(`${(0, utils_1.colorize)('[✗]', 'red')} ${res.provider} ERROR: ${res.message}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
console.log('\n=== Drift Detection ===\n');
|
|
168
|
+
for (const res of results) {
|
|
169
|
+
if (res.status === 'no-drift') {
|
|
170
|
+
console.log(`${(0, utils_1.colorize)('[✓]', 'green')} ${res.provider} — no drift`);
|
|
171
|
+
}
|
|
172
|
+
else if (res.status === 'no-baseline') {
|
|
173
|
+
console.log(`${(0, utils_1.colorize)('[ ]', 'dim')} ${res.provider} — ${res.message}`);
|
|
174
|
+
}
|
|
175
|
+
else if (res.status === 'skipped') {
|
|
176
|
+
console.log(`${(0, utils_1.colorize)('[ ]', 'dim')} ${res.provider} — ${res.message}`);
|
|
177
|
+
}
|
|
178
|
+
else if (res.status === 'drift' && res.report) {
|
|
179
|
+
console.log(`${(0, utils_1.colorize)('[⚠]', 'yellow')} ${res.provider} — drift detected:`);
|
|
180
|
+
if (res.report.addedKeys.length > 0) {
|
|
181
|
+
console.log(` ${(0, utils_1.colorize)('+', 'green')} new keys: ${res.report.addedKeys.join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
if (res.report.removedKeys.length > 0) {
|
|
184
|
+
console.log(` ${(0, utils_1.colorize)('-', 'red')} removed keys: ${res.report.removedKeys.join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
if (res.report.typeChanges.length > 0) {
|
|
187
|
+
for (const change of res.report.typeChanges) {
|
|
188
|
+
console.log(` ${(0, utils_1.colorize)('!', 'yellow')} type changed: ${change.key} (${change.oldType} → ${change.newType})`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (res.report.statusChanged) {
|
|
192
|
+
console.log(` ${(0, utils_1.colorize)('!', 'yellow')} status changed: ${res.report.statusChanged.old} → ${res.report.statusChanged.new}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (res.status === 'error') {
|
|
196
|
+
console.log(`${(0, utils_1.colorize)('[✗]', 'red')} ${res.provider} ERROR: ${res.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const driftCount = results.filter((r) => r.status === 'drift').length;
|
|
202
|
+
const errorCount = results.filter((r) => r.status === 'error').length;
|
|
203
|
+
if (errorCount > 0) {
|
|
204
|
+
process.exit(2);
|
|
205
|
+
}
|
|
206
|
+
else if (driftCount > 0) {
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runReconcileCommand(args: string[]): Promise<void>;
|