@paychainly/cli 1.0.4 → 1.1.1

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 ADDED
@@ -0,0 +1,149 @@
1
+ # @paychainly/cli
2
+
3
+ > Relay live Paychainly webhooks to your local server during development — no public URL needed.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@paychainly/cli)](https://www.npmjs.com/package/@paychainly/cli)
6
+ [![license](https://img.shields.io/npm/l/@paychainly/cli)](LICENSE)
7
+
8
+ ---
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @paychainly/cli
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ paychainly listen --api-key pk_live_... --forward-to http://localhost:3000/webhook
22
+ ```
23
+
24
+ Webhooks fired on [api.paychainly.com](https://api.paychainly.com) are forwarded in real time to your local server.
25
+
26
+ ---
27
+
28
+ ## Commands
29
+
30
+ ### `listen` — relay live webhooks
31
+
32
+ ```bash
33
+ paychainly listen [options]
34
+ ```
35
+
36
+ | Option | Description | Default |
37
+ |---|---|---|
38
+ | `--api-key` | Your Paychainly API key (`pk_live_...` or `pk_test_...`) | `PAYCHAINLY_API_KEY` env |
39
+ | `--forward-to` | Full local URL to POST webhooks to | `http://localhost:3000/webhook` |
40
+ | `--port` | Shorthand for `--forward-to http://localhost:<port>/webhook` | `3000` |
41
+ | `--host` | Paychainly server URL | `https://api.paychainly.com` |
42
+ | `--secret` | Webhook secret — enables HMAC-SHA256 signature verification | — |
43
+ | `--verbose` | Pretty-print the full webhook payload (JSON) | off |
44
+ | `--filter` | Only relay events matching this name (e.g. `deposit_detected`) | all events |
45
+ | `--log` | Append every received event to a JSONL file for later replay | — |
46
+
47
+ **Examples**
48
+
49
+ ```bash
50
+ # Basic
51
+ paychainly listen --api-key pk_live_... --port 3000
52
+
53
+ # Show full payload for every event
54
+ paychainly listen --api-key pk_live_... --verbose
55
+
56
+ # Only relay deposit events and save them to disk
57
+ paychainly listen --api-key pk_live_... \
58
+ --filter deposit_detected \
59
+ --log deposits.jsonl
60
+
61
+ # Verify webhook signatures
62
+ paychainly listen --api-key pk_live_... --secret whsec_...
63
+ ```
64
+
65
+ ---
66
+
67
+ ### `status` — check API key & connectivity
68
+
69
+ ```bash
70
+ paychainly status --api-key pk_live_...
71
+ ```
72
+
73
+ Connects to the server, validates the key, and prints the user ID and account mode. Useful for confirming your key is correct before starting a session.
74
+
75
+ ---
76
+
77
+ ### `replay` — resend saved events to your local server
78
+
79
+ ```bash
80
+ paychainly replay --log events.jsonl [options]
81
+ ```
82
+
83
+ | Option | Description | Default |
84
+ |---|---|---|
85
+ | `--log` | JSONL file produced by `listen --log` **(required)** | — |
86
+ | `--forward-to` | Local URL to POST events to | `http://localhost:3000/webhook` |
87
+ | `--port` | Shorthand for `--forward-to http://localhost:<port>/webhook` | `3000` |
88
+ | `--filter` | Only replay events matching this name | all events |
89
+ | `--delay` | Milliseconds between replayed events | `500` |
90
+ | `--verbose` | Pretty-print each payload before sending | off |
91
+
92
+ **Examples**
93
+
94
+ ```bash
95
+ # Replay everything
96
+ paychainly replay --log events.jsonl
97
+
98
+ # Replay only deposit events with a 1-second gap, show payloads
99
+ paychainly replay --log events.jsonl \
100
+ --filter deposit_detected \
101
+ --delay 1000 \
102
+ --verbose
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Typical Developer Workflow
108
+
109
+ ```bash
110
+ # 1. Start listening and capture events to a file
111
+ paychainly listen --api-key pk_live_... --log events.jsonl --verbose
112
+
113
+ # 2. Trigger a test payment on your dashboard
114
+ # 3. Inspect your webhook handler logs
115
+
116
+ # 4. Fix a bug in your handler, restart your server
117
+
118
+ # 5. Replay the captured events — no real payment needed
119
+ paychainly replay --log events.jsonl --filter deposit_detected
120
+ ```
121
+
122
+ ---
123
+
124
+ ## Environment Variables
125
+
126
+ You can set these instead of passing flags every time:
127
+
128
+ ```bash
129
+ export PAYCHAINLY_API_KEY=pk_live_...
130
+ export PAYCHAINLY_HOST=https://api.paychainly.com # optional override
131
+ export PAYCHAINLY_WEBHOOK_SECRET=whsec_... # optional
132
+ ```
133
+
134
+ ---
135
+
136
+ ## Webhook Events
137
+
138
+ | Event | Description |
139
+ |---|---|
140
+ | `deposit_detected` | A USDT transfer to a monitored deposit address was confirmed |
141
+ | `sweep_completed` | Funds swept from deposit address to master wallet |
142
+ | `address_expired` | A deposit address session expired without a payment |
143
+ | `withdrawal_completed` | A user-initiated withdrawal was processed |
144
+
145
+ ---
146
+
147
+ ## License
148
+
149
+ MIT © [Paychainly](https://paychainly.com)
package/bin/paychainly.js CHANGED
@@ -1,28 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const [,, cmd, ...argv] = process.argv;
4
+
5
+ if (cmd === '--version' || cmd === '-V') {
6
+ console.log(require('../package.json').version);
7
+ process.exit(0);
8
+ }
9
+
4
10
  if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
11
+ const v = require('../package.json').version;
5
12
  console.log(`
6
- Paychainly CLI
13
+ Paychainly CLI v${v}
7
14
 
8
15
  Commands:
9
- listen Relay webhooks to your local server
16
+ listen Relay live webhooks to your local server
17
+ status Check API key validity and server connectivity
18
+ replay Replay saved webhook events to your local server
10
19
 
11
20
  Usage:
12
- paychainly listen --api-key <key> --port <port>
13
21
  paychainly listen --api-key <key> --forward-to http://localhost:3000/webhook
22
+ paychainly listen --api-key <key> --port 3000 --verbose
23
+ paychainly listen --api-key <key> --filter deposit_detected --log events.jsonl
24
+ paychainly status --api-key <key>
25
+ paychainly replay --log events.jsonl --forward-to http://localhost:3000/webhook
14
26
 
15
- Options:
27
+ Options (listen):
16
28
  --api-key Your Paychainly API key (pk_live_... or pk_test_...)
17
29
  --port Local port to forward webhooks to (default: 3000)
18
30
  --forward-to Full local URL to forward webhooks to
19
31
  --host Paychainly server URL (default: https://api.paychainly.com)
20
32
  --secret Webhook secret to verify incoming signatures
33
+ --verbose Print full webhook payload as pretty JSON
34
+ --filter Only relay events matching this name (e.g. deposit_detected)
35
+ --log Append all received events to a JSONL file for later replay
36
+
37
+ Options (status):
38
+ --api-key Your Paychainly API key
39
+ --host Paychainly server URL (default: https://api.paychainly.com)
40
+
41
+ Options (replay):
42
+ --log JSONL file produced by --log (required)
43
+ --forward-to Local URL to forward to (default: http://localhost:3000/webhook)
44
+ --port Local port shorthand
45
+ --filter Only replay events matching this name
46
+ --delay Milliseconds between replayed events (default: 500)
47
+ --verbose Print full payload for each replayed event
21
48
  `);
22
49
  process.exit(0);
23
50
  }
51
+
24
52
  if (cmd === 'listen') {
25
53
  require('../src/listen.js')(argv);
54
+ } else if (cmd === 'status') {
55
+ require('../src/status.js')(argv);
56
+ } else if (cmd === 'replay') {
57
+ require('../src/replay.js')(argv);
26
58
  } else {
27
59
  console.error(`Unknown command: ${cmd}. Run "paychainly help" for usage.`);
28
60
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paychainly/cli",
3
- "version": "1.0.4",
3
+ "version": "1.1.1",
4
4
  "description": "Paychainly CLI — relay webhooks to your localhost during development",
5
5
  "keywords": [
6
6
  "paychainly",
package/src/listen.js CHANGED
@@ -4,20 +4,20 @@ const { io } = require('socket.io-client');
4
4
  const http = require('http');
5
5
  const https = require('https');
6
6
  const crypto = require('crypto');
7
+ const fs = require('fs');
7
8
 
8
- // ── ANSI colours ──────────────────────────────────────────────────────────────
9
9
  const c = {
10
- reset: '\x1b[0m',
11
- bold: '\x1b[1m',
12
- dim: '\x1b[2m',
13
- green: '\x1b[32m',
14
- cyan: '\x1b[36m',
15
- yellow: '\x1b[33m',
16
- red: '\x1b[31m',
17
- magenta:'\x1b[35m',
18
- blue: '\x1b[34m',
19
- white: '\x1b[37m',
20
- gray: '\x1b[90m',
10
+ reset: '\x1b[0m',
11
+ bold: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+ green: '\x1b[32m',
14
+ cyan: '\x1b[36m',
15
+ yellow: '\x1b[33m',
16
+ red: '\x1b[31m',
17
+ magenta: '\x1b[35m',
18
+ blue: '\x1b[34m',
19
+ white: '\x1b[37m',
20
+ gray: '\x1b[90m',
21
21
  };
22
22
 
23
23
  function parseArgs(argv) {
@@ -65,7 +65,7 @@ function postToLocal(url, payload) {
65
65
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
66
66
  }, (res) => {
67
67
  let data = '';
68
- res.on('data', c => data += c);
68
+ res.on('data', ch => data += ch);
69
69
  res.on('end', () => resolve({ statusCode: res.statusCode, body: data, ms: Date.now() - start }));
70
70
  });
71
71
  req.on('error', (err) => resolve({ statusCode: 0, body: err.message, ms: Date.now() - start }));
@@ -80,26 +80,41 @@ function now() {
80
80
  }
81
81
 
82
82
  function eventColor(event) {
83
- if (event?.includes('deposit')) return c.green;
84
- if (event?.includes('sweep')) return c.cyan;
83
+ if (event?.includes('deposit')) return c.green;
84
+ if (event?.includes('sweep')) return c.cyan;
85
85
  if (event?.includes('withdrawal')) return c.blue;
86
- if (event?.includes('address')) return c.magenta;
86
+ if (event?.includes('address')) return c.magenta;
87
87
  return c.white;
88
88
  }
89
89
 
90
+ function prettyJson(obj) {
91
+ return JSON.stringify(obj, null, 2)
92
+ .split('\n')
93
+ .map(l => ' ' + l)
94
+ .join('\n');
95
+ }
96
+
90
97
  module.exports = function listen(argv) {
91
- const args = parseArgs(argv);
92
- const apiKey = args['api-key'] || process.env.PAYCHAINLY_API_KEY;
93
- const host = args['host'] || process.env.PAYCHAINLY_HOST || 'https://api.paychainly.com';
94
- const secret = args['secret'] || process.env.PAYCHAINLY_WEBHOOK_SECRET || '';
98
+ const args = parseArgs(argv);
99
+ const apiKey = args['api-key'] || process.env.PAYCHAINLY_API_KEY;
100
+ const host = args['host'] || process.env.PAYCHAINLY_HOST || 'https://api.paychainly.com';
101
+ const secret = args['secret'] || process.env.PAYCHAINLY_WEBHOOK_SECRET || '';
95
102
  const forwardTo = args['forward-to'] || (args['port'] ? `http://localhost:${args['port']}/webhook` : 'http://localhost:3000/webhook');
103
+ const verbose = !!args['verbose'];
104
+ const filter = args['filter'] || null;
105
+ const logPath = args['log'] || null;
96
106
 
97
107
  if (!apiKey) {
98
108
  console.error(`${c.red}✗ Missing --api-key. Run: paychainly listen --api-key <pk_live_...>${c.reset}`);
99
109
  process.exit(1);
100
110
  }
101
111
 
102
- // ── Header ────────────────────────────────────────────────────────────────
112
+ let logStream = null;
113
+ if (logPath) {
114
+ logStream = fs.createWriteStream(logPath, { flags: 'a' });
115
+ logStream.on('error', (err) => console.error(`${c.red}✗ Log file error: ${err.message}${c.reset}`));
116
+ }
117
+
103
118
  console.log(`
104
119
  ${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
105
120
  ║ Paychainly CLI Relay ║
@@ -107,11 +122,15 @@ ${c.bold}${c.cyan} ╔═══════════════════
107
122
  `);
108
123
  console.log(` ${c.gray}Server :${c.reset} ${host}`);
109
124
  console.log(` ${c.gray}Forward :${c.reset} ${c.cyan}${forwardTo}${c.reset}`);
110
- if (secret) console.log(` ${c.gray}Secret :${c.reset} ${c.green}✓ signature verification enabled${c.reset}`);
125
+ if (filter) console.log(` ${c.gray}Filter :${c.reset} ${c.yellow}${filter}${c.reset}`);
126
+ if (logPath) console.log(` ${c.gray}Log :${c.reset} ${logPath}`);
127
+ if (verbose) console.log(` ${c.gray}Mode :${c.reset} ${c.cyan}verbose${c.reset}`);
128
+ if (secret) console.log(` ${c.gray}Secret :${c.reset} ${c.green}✓ signature verification enabled${c.reset}`);
111
129
  console.log(` ${c.gray}API key :${c.reset} ${apiKey.slice(0, 12)}${'•'.repeat(8)}\n`);
112
130
 
113
131
  let connected = false;
114
132
  let eventCount = 0;
133
+ const eventStats = {};
115
134
 
116
135
  function connect() {
117
136
  const socket = io(`${host}/relay`, {
@@ -137,7 +156,7 @@ ${c.bold}${c.cyan} ╔═══════════════════
137
156
  console.log(`\n ${c.yellow}⚠ Replaced by a newer connection.${c.reset}`);
138
157
  });
139
158
 
140
- socket.on('connect_error', (err) => {
159
+ socket.on('connect_error', () => {
141
160
  if (!connected) {
142
161
  process.stdout.write(` ${c.yellow}⟳ Connecting to ${host}...${c.reset}\r`);
143
162
  }
@@ -156,16 +175,34 @@ ${c.bold}${c.cyan} ╔═══════════════════
156
175
  });
157
176
 
158
177
  socket.on('webhook', async ({ relayId, payload, targetUrl }) => {
178
+ // Skip events that don't match the filter (ack silently)
179
+ if (filter && payload?.event !== filter) {
180
+ socket.emit('webhook_response', { relayId, statusCode: 200, body: '' });
181
+ return;
182
+ }
183
+
159
184
  eventCount++;
160
- const target = forwardTo || targetUrl;
185
+ const evtName = payload?.event || 'unknown';
186
+ eventStats[evtName] = (eventStats[evtName] || 0) + 1;
187
+
188
+ const target = forwardTo || targetUrl;
161
189
  const sigValid = secret ? verifySignature(payload, secret) : null;
162
- const ec = eventColor(payload?.event);
163
- const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
190
+ const ec = eventColor(payload?.event);
191
+ const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
164
192
 
165
193
  console.log(divider);
166
- console.log(` ${c.bold}${ec}${payload?.event || 'unknown'}${c.reset} ${c.gray}${now()}${c.reset} ${c.gray}#${eventCount}${c.reset}`);
194
+ console.log(` ${c.bold}${ec}${evtName}${c.reset} ${c.gray}${now()}${c.reset} ${c.gray}#${eventCount}${c.reset}`);
195
+
167
196
  if (sigValid === true) console.log(` ${c.green}🔐 Signature valid${c.reset}`);
168
197
  if (sigValid === false) console.log(` ${c.red}🔐 Signature INVALID${c.reset}`);
198
+
199
+ if (verbose) {
200
+ console.log(`\n${c.gray}${prettyJson(payload)}${c.reset}\n`);
201
+ }
202
+
203
+ // Write to log file before forwarding
204
+ if (logStream) logStream.write(JSON.stringify(payload) + '\n');
205
+
169
206
  console.log(` ${c.gray}→${c.reset} ${target}`);
170
207
 
171
208
  const result = await postToLocal(target, payload);
@@ -176,7 +213,8 @@ ${c.bold}${c.cyan} ╔═══════════════════
176
213
  } else if (result.statusCode === 0) {
177
214
  console.log(` ${c.red}✗ Connection refused — is your local server running?${c.reset}`);
178
215
  } else {
179
- console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset} ${c.gray}${result.body?.slice(0, 80)}${c.reset}`);
216
+ console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset}`);
217
+ if (result.body) console.log(` ${c.gray} ${result.body.slice(0, 200)}${c.reset}`);
180
218
  }
181
219
 
182
220
  socket.emit('webhook_response', {
@@ -194,7 +232,15 @@ ${c.bold}${c.cyan} ╔═══════════════════
194
232
  process.on('SIGINT', () => {
195
233
  console.log(`\n\n ${c.gray}Closing relay...${c.reset}`);
196
234
  socket.disconnect();
197
- console.log(` ${c.green}✓ ${eventCount} event(s) relayed${c.reset}\n`);
235
+ if (logStream) logStream.end();
236
+ console.log(` ${c.green}✓ ${eventCount} event(s) relayed${c.reset}`);
237
+ if (eventCount > 0) {
238
+ Object.entries(eventStats).forEach(([evt, count]) => {
239
+ console.log(` ${c.gray} ${evt}: ${count}${c.reset}`);
240
+ });
241
+ if (logPath) console.log(` ${c.gray} saved → ${logPath}${c.reset}`);
242
+ }
243
+ console.log();
198
244
  process.exit(0);
199
245
  });
200
246
  };
package/src/replay.js ADDED
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const http = require('http');
5
+ const https = require('https');
6
+
7
+ const c = {
8
+ reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m', cyan: '\x1b[36m',
9
+ red: '\x1b[31m', gray: '\x1b[90m', yellow: '\x1b[33m', white: '\x1b[37m',
10
+ magenta: '\x1b[35m', blue: '\x1b[34m',
11
+ };
12
+
13
+ function parseArgs(argv) {
14
+ const args = {};
15
+ for (let i = 0; i < argv.length; i++) {
16
+ if (argv[i].startsWith('--')) {
17
+ const key = argv[i].slice(2);
18
+ args[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
19
+ }
20
+ }
21
+ return args;
22
+ }
23
+
24
+ function postToLocal(url, payload) {
25
+ return new Promise((resolve) => {
26
+ const body = JSON.stringify(payload);
27
+ const parsed = new URL(url);
28
+ const lib = parsed.protocol === 'https:' ? https : http;
29
+ const start = Date.now();
30
+ const req = lib.request({
31
+ hostname: parsed.hostname,
32
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
33
+ path: parsed.pathname + parsed.search,
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
36
+ }, (res) => {
37
+ let data = '';
38
+ res.on('data', ch => data += ch);
39
+ res.on('end', () => resolve({ statusCode: res.statusCode, body: data, ms: Date.now() - start }));
40
+ });
41
+ req.on('error', (err) => resolve({ statusCode: 0, body: err.message, ms: Date.now() - start }));
42
+ req.setTimeout(30_000, () => { req.destroy(); resolve({ statusCode: 0, body: 'timeout', ms: Date.now() - start }); });
43
+ req.write(body);
44
+ req.end();
45
+ });
46
+ }
47
+
48
+ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
49
+
50
+ function eventColor(event) {
51
+ if (event?.includes('deposit')) return c.green;
52
+ if (event?.includes('sweep')) return c.cyan;
53
+ if (event?.includes('withdrawal')) return c.blue;
54
+ if (event?.includes('address')) return c.magenta;
55
+ return c.white;
56
+ }
57
+
58
+ function prettyJson(obj) {
59
+ return JSON.stringify(obj, null, 2).split('\n').map(l => ' ' + l).join('\n');
60
+ }
61
+
62
+ module.exports = async function replay(argv) {
63
+ const args = parseArgs(argv);
64
+ const logFile = args['log'];
65
+ const forwardTo = args['forward-to'] || (args['port'] ? `http://localhost:${args['port']}/webhook` : 'http://localhost:3000/webhook');
66
+ const filter = args['filter'] || null;
67
+ const delay = parseInt(args['delay'] || '500', 10);
68
+ const verbose = !!args['verbose'];
69
+
70
+ if (!logFile) {
71
+ console.error(`${c.red}✗ Missing --log <file>. Run: paychainly replay --log events.jsonl${c.reset}`);
72
+ process.exit(1);
73
+ }
74
+
75
+ let lines;
76
+ try {
77
+ lines = fs.readFileSync(logFile, 'utf8').split('\n').filter(Boolean);
78
+ } catch {
79
+ console.error(`${c.red}✗ Cannot read file: ${logFile}${c.reset}`);
80
+ process.exit(1);
81
+ }
82
+
83
+ const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
84
+ const toReplay = filter ? events.filter(e => e.event === filter) : events;
85
+
86
+ console.log(`
87
+ ${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
88
+ ║ Paychainly CLI Replay ║
89
+ ╚═══════════════════════════════════════╝${c.reset}
90
+ `);
91
+ console.log(` ${c.gray}File :${c.reset} ${logFile}`);
92
+ console.log(` ${c.gray}Forward :${c.reset} ${c.cyan}${forwardTo}${c.reset}`);
93
+ console.log(` ${c.gray}Events :${c.reset} ${toReplay.length}${filter ? ` ${c.yellow}(filter: ${filter})${c.reset}` : ''}`);
94
+ console.log(` ${c.gray}Delay :${c.reset} ${delay}ms between events\n`);
95
+
96
+ if (toReplay.length === 0) {
97
+ console.log(` ${c.yellow}No events to replay.${c.reset}\n`);
98
+ return;
99
+ }
100
+
101
+ let ok = 0, fail = 0;
102
+
103
+ for (let i = 0; i < toReplay.length; i++) {
104
+ const payload = toReplay[i];
105
+ const evtName = payload?.event || 'unknown';
106
+ const ec = eventColor(evtName);
107
+ const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
108
+
109
+ console.log(divider);
110
+ console.log(` ${c.bold}${ec}${evtName}${c.reset} ${c.gray}#${i + 1}/${toReplay.length}${c.reset}`);
111
+
112
+ if (verbose) {
113
+ console.log(`\n${c.gray}${prettyJson(payload)}${c.reset}\n`);
114
+ }
115
+
116
+ console.log(` ${c.gray}→${c.reset} ${forwardTo}`);
117
+
118
+ const result = await postToLocal(forwardTo, payload);
119
+ const isOk = result.statusCode >= 200 && result.statusCode < 300;
120
+
121
+ if (isOk) {
122
+ ok++;
123
+ console.log(` ${c.green}✓ ${result.statusCode} ${result.ms}ms${c.reset}`);
124
+ } else if (result.statusCode === 0) {
125
+ fail++;
126
+ console.log(` ${c.red}✗ Connection refused — is your local server running?${c.reset}`);
127
+ } else {
128
+ fail++;
129
+ console.log(` ${c.red}✗ ${result.statusCode} ${result.ms}ms${c.reset}`);
130
+ if (result.body) console.log(` ${c.gray} ${result.body.slice(0, 200)}${c.reset}`);
131
+ }
132
+
133
+ if (i < toReplay.length - 1) await sleep(delay);
134
+ }
135
+
136
+ const divider = c.gray + ' ' + '─'.repeat(56) + c.reset;
137
+ console.log(divider);
138
+ console.log(`\n ${c.green}✓ ${ok} delivered${c.reset}${fail ? ` ${c.red}✗ ${fail} failed${c.reset}` : ''}\n`);
139
+ };
package/src/status.js ADDED
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const { io } = require('socket.io-client');
4
+
5
+ const c = {
6
+ reset: '\x1b[0m', bold: '\x1b[1m', green: '\x1b[32m',
7
+ cyan: '\x1b[36m', red: '\x1b[31m', gray: '\x1b[90m', yellow: '\x1b[33m',
8
+ };
9
+
10
+ function parseArgs(argv) {
11
+ const args = {};
12
+ for (let i = 0; i < argv.length; i++) {
13
+ if (argv[i].startsWith('--')) {
14
+ const key = argv[i].slice(2);
15
+ args[key] = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[++i] : true;
16
+ }
17
+ }
18
+ return args;
19
+ }
20
+
21
+ module.exports = function status(argv) {
22
+ const args = parseArgs(argv);
23
+ const apiKey = args['api-key'] || process.env.PAYCHAINLY_API_KEY;
24
+ const host = args['host'] || process.env.PAYCHAINLY_HOST || 'https://api.paychainly.com';
25
+
26
+ if (!apiKey) {
27
+ console.error(`${c.red}✗ Missing --api-key${c.reset}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log(`\n ${c.bold}${c.cyan}Paychainly Status Check${c.reset}\n`);
32
+ console.log(` ${c.gray}Server :${c.reset} ${host}`);
33
+ console.log(` ${c.gray}API key :${c.reset} ${apiKey.slice(0, 12)}${'•'.repeat(8)}\n`);
34
+
35
+ process.stdout.write(` ${c.yellow}⟳ Connecting...${c.reset}\r`);
36
+
37
+ const socket = io(`${host}/relay`, {
38
+ auth: { apiKey },
39
+ transports: ['websocket'],
40
+ reconnection: false,
41
+ timeout: 10000,
42
+ });
43
+
44
+ socket.on('relay_connected', ({ userId, mode }) => {
45
+ process.stdout.write(' '.repeat(40) + '\r');
46
+ console.log(` ${c.green}✓ API key valid${c.reset}`);
47
+ console.log(` ${c.gray}User ID :${c.reset} ${userId}`);
48
+ console.log(` ${c.gray}Mode :${c.reset} ${mode}\n`);
49
+ socket.disconnect();
50
+ process.exit(0);
51
+ });
52
+
53
+ socket.on('relay_error', ({ message }) => {
54
+ process.stdout.write(' '.repeat(40) + '\r');
55
+ console.error(` ${c.red}✗ ${message}${c.reset}\n`);
56
+ socket.disconnect();
57
+ process.exit(1);
58
+ });
59
+
60
+ socket.on('connect_error', (err) => {
61
+ process.stdout.write(' '.repeat(40) + '\r');
62
+ console.error(` ${c.red}✗ Cannot reach server: ${err.message}${c.reset}\n`);
63
+ process.exit(1);
64
+ });
65
+
66
+ setTimeout(() => {
67
+ console.error(`\n ${c.red}✗ Timed out after 10s — server unreachable${c.reset}\n`);
68
+ process.exit(1);
69
+ }, 10000);
70
+ };