mayar 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 Frianto Moerdowo
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,153 @@
1
+ # mayar
2
+
3
+ Command-line interface for the [Mayar](https://docs.mayar.id) API. Production-only (`https://api.mayar.id`). Zero runtime dependencies — Node.js 18+ stdlib only.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g mayar
9
+ # or run without installing:
10
+ npx mayar --help
11
+ ```
12
+
13
+ Or from source:
14
+
15
+ ```bash
16
+ git clone https://github.com/moerdowo/mayar-cli.git
17
+ cd mayar-cli
18
+ npm link # exposes a `mayar` command on your PATH
19
+ ```
20
+
21
+ ## First run
22
+
23
+ The first time you invoke any command, the CLI prints an ASCII banner and asks for your production API key (input is masked). Paste it once; it's stored at `~/.mayar/config.json` (chmod 600) and reused on subsequent runs.
24
+
25
+ ```bash
26
+ mayar balance
27
+ # ███╗ ███╗ █████╗ ██╗ ██╗ █████╗ ██████╗
28
+ # ...
29
+ # Welcome to Mayar CLI.
30
+ # No API key found. Get yours from https://web.mayar.id → Integration → API Key.
31
+ # Paste your production API key: ************
32
+ # ✓ Saved to /Users/you/.mayar/config.json
33
+ ```
34
+
35
+ You can also run `mayar init` explicitly to (re-)configure the key, or pass `--api-key <key>` on any invocation to override.
36
+
37
+ ## Commands
38
+
39
+ ```
40
+ Setup
41
+ init Run first-time setup
42
+ config show Show masked saved key
43
+ config reset Remove the saved key
44
+
45
+ Account
46
+ balance GET /hl/v1/balance
47
+
48
+ Invoices
49
+ invoice list [--page N --pageSize N] GET /hl/v1/invoice
50
+ invoice get <id> GET /hl/v1/invoice/{id}
51
+ invoice close <id> GET /hl/v1/invoice/close/{id}
52
+ invoice reopen <id> GET /hl/v1/invoice/open/{id}
53
+ invoice create --data <json|@file> POST /hl/v1/invoice/create
54
+
55
+ Products
56
+ product list [--page N --pageSize N] GET /hl/v1/product
57
+ product search <keyword> GET /hl/v1/product?search=...
58
+ product type <type> [--page ...] GET /hl/v1/product/type/{type}
59
+ product get <id> GET /hl/v1/product/{id}
60
+ product close <id> GET /hl/v1/product/close/{id}
61
+ product reopen <id> GET /hl/v1/product/open/{id}
62
+
63
+ Single payment requests
64
+ payment list GET /hl/v1/payment
65
+ payment get <id> GET /hl/v1/payment/{id}
66
+ payment close <id> GET /hl/v1/payment/close/{id}
67
+ payment reopen <id> GET /hl/v1/payment/open/{id}
68
+ payment create --data <json|@file> POST /hl/v1/payment/create
69
+
70
+ Customers
71
+ customer list [--page ...] GET /hl/v1/customer
72
+ customer create --data <json|@file> POST /hl/v1/customer/create
73
+
74
+ Transactions
75
+ tx list [--page ...] GET /hl/v1/transactions (paid)
76
+ tx unpaid [--page ...] GET /hl/v1/transactions/unpaid
77
+
78
+ Dynamic QR
79
+ qrcode <amount> POST /hl/v1/qrcode/create
80
+
81
+ Webhooks
82
+ webhook register <url> GET /hl/v1/webhook/register
83
+ webhook test <url> POST /hl/v1/webhook/test
84
+ webhook history [--page ...] GET /hl/v1/webhook/history
85
+
86
+ Global flags
87
+ --json Output raw JSON instead of pretty tables
88
+ --api-key <key> Override saved key for this run
89
+ --page N Pagination page (default 1)
90
+ --pageSize N Pagination page size (default 10)
91
+ -h, --help Show help
92
+ -v, --version Show version
93
+ ```
94
+
95
+ ## Examples
96
+
97
+ ```bash
98
+ # Account balance
99
+ mayar balance
100
+
101
+ # Paginated lists
102
+ mayar invoice list --page 1 --pageSize 20
103
+ mayar product type ebook --pageSize 50
104
+
105
+ # Search
106
+ mayar product search "kelas python"
107
+
108
+ # Create an invoice from a JSON file
109
+ cat > /tmp/inv.json <<'JSON'
110
+ {
111
+ "name": "Andre",
112
+ "email": "andre@example.com",
113
+ "mobile": "08123456789",
114
+ "redirectUrl": "https://example.com/thanks",
115
+ "description": "Order #1234",
116
+ "expiredAt": "2026-12-31T23:59:59.000Z",
117
+ "items": [{ "quantity": 1, "rate": 50000, "description": "1x Course" }]
118
+ }
119
+ JSON
120
+ mayar invoice create --data @/tmp/inv.json
121
+
122
+ # Create a customer inline
123
+ mayar customer create --data '{"name":"Raihan","email":"r@example.com","mobile":"081234567890"}'
124
+
125
+ # Create a payment request
126
+ mayar payment create --data '{"name":"X","email":"x@y.com","amount":170000,"mobile":"08123","redirectUrl":"https://m.com","description":"Test","expiredAt":"2026-12-31T00:00:00.000Z"}'
127
+
128
+ # Dynamic QR for IDR 10,000
129
+ mayar qrcode 10000
130
+
131
+ # Webhooks
132
+ mayar webhook register https://example.com/hooks/mayar
133
+ mayar webhook test https://example.com/hooks/mayar
134
+ mayar webhook history --page 1 --pageSize 20
135
+
136
+ # Pipe raw JSON to jq
137
+ mayar invoice list --json | jq '.data[] | {id, status}'
138
+ ```
139
+
140
+ ## Config
141
+
142
+ | Key | Value |
143
+ | -------- | ------------------------------------------ |
144
+ | Path | `~/.mayar/config.json` (chmod 600) |
145
+ | Endpoint | `https://api.mayar.id` (production, fixed) |
146
+
147
+ To rotate keys: `mayar config reset && mayar init`.
148
+
149
+ ## Notes
150
+
151
+ - Sandbox is intentionally not supported — this CLI hits production only.
152
+ - All requests use `Authorization: Bearer <key>`. Errors print `API <status> — <message>` and exit non-zero.
153
+ - `--data @file.json` reads from disk; `--data '{...}'` reads inline JSON.
package/bin/mayar.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../src/cli.js').run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "mayar",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency Node.js CLI for the Mayar API (production endpoint).",
5
+ "bin": {
6
+ "mayar": "./bin/mayar.js"
7
+ },
8
+ "main": "src/cli.js",
9
+ "scripts": {
10
+ "start": "node bin/mayar.js"
11
+ },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "keywords": [
16
+ "mayar",
17
+ "mayar-api",
18
+ "payments",
19
+ "invoice",
20
+ "cli",
21
+ "indonesia",
22
+ "qris"
23
+ ],
24
+ "author": "Frianto Moerdowo <frianto@duck.com>",
25
+ "license": "MIT",
26
+ "homepage": "https://github.com/moerdowo/mayar-cli#readme",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/moerdowo/mayar-cli.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/moerdowo/mayar-cli/issues"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "src",
37
+ "README.md",
38
+ "LICENSE"
39
+ ]
40
+ }
package/src/api.js ADDED
@@ -0,0 +1,46 @@
1
+ const https = require('https');
2
+ const { URL } = require('url');
3
+
4
+ const BASE_URL = 'https://api.mayar.id';
5
+ const VERSION = require('../package.json').version;
6
+
7
+ function request(method, pathname, { apiKey, body, query } = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ const url = new URL(BASE_URL + pathname);
10
+ if (query) {
11
+ for (const [k, v] of Object.entries(query)) {
12
+ if (v !== undefined && v !== null && v !== '') url.searchParams.set(k, v);
13
+ }
14
+ }
15
+ const data = body ? Buffer.from(JSON.stringify(body)) : null;
16
+ const opts = {
17
+ method,
18
+ hostname: url.hostname,
19
+ path: url.pathname + url.search,
20
+ headers: {
21
+ Authorization: `Bearer ${apiKey}`,
22
+ Accept: 'application/json',
23
+ 'User-Agent': `mayar-cli/${VERSION}`,
24
+ },
25
+ };
26
+ if (data) {
27
+ opts.headers['Content-Type'] = 'application/json';
28
+ opts.headers['Content-Length'] = data.length;
29
+ }
30
+ const req = https.request(opts, (res) => {
31
+ const chunks = [];
32
+ res.on('data', (c) => chunks.push(c));
33
+ res.on('end', () => {
34
+ const text = Buffer.concat(chunks).toString('utf8');
35
+ let parsed;
36
+ try { parsed = text ? JSON.parse(text) : null; } catch (_) { parsed = text; }
37
+ resolve({ status: res.statusCode, body: parsed, raw: text });
38
+ });
39
+ });
40
+ req.on('error', reject);
41
+ if (data) req.write(data);
42
+ req.end();
43
+ });
44
+ }
45
+
46
+ module.exports = { request, BASE_URL };
package/src/cli.js ADDED
@@ -0,0 +1,176 @@
1
+ const ui = require('./ui');
2
+ const config = require('./config');
3
+
4
+ const VERSION = require('../package.json').version;
5
+
6
+ const HELP = () => `${ui.bold('mayar')} ${ui.dim('— Mayar API CLI (production)')}
7
+
8
+ ${ui.bold('Usage:')}
9
+ mayar <command> [args] [flags]
10
+
11
+ ${ui.bold('Setup:')}
12
+ init Run first-time setup (or re-configure API key)
13
+ config show Show config path and masked API key
14
+ config reset Remove the saved API key
15
+
16
+ ${ui.bold('Account:')}
17
+ balance Get account balance
18
+
19
+ ${ui.bold('Invoices:')}
20
+ invoice list [--page N --pageSize N]
21
+ invoice get <id>
22
+ invoice close <id>
23
+ invoice reopen <id>
24
+ invoice create --data <json|@file.json>
25
+
26
+ ${ui.bold('Products:')}
27
+ product list [--page N --pageSize N]
28
+ product search <keyword>
29
+ product type <ebook|course|membership|saas|event|webinar|...>
30
+ product get <id>
31
+ product close <id>
32
+ product reopen <id>
33
+
34
+ ${ui.bold('Single payment requests:')}
35
+ payment list
36
+ payment get <id>
37
+ payment close <id>
38
+ payment reopen <id>
39
+ payment create --data <json|@file.json>
40
+
41
+ ${ui.bold('Customers:')}
42
+ customer list [--page N --pageSize N]
43
+ customer create --data <json|@file.json>
44
+
45
+ ${ui.bold('Transactions:')}
46
+ tx list [--page N --pageSize N] Paid transactions
47
+ tx unpaid [--page N --pageSize N] Unpaid transactions
48
+
49
+ ${ui.bold('Dynamic QR:')}
50
+ qrcode <amount>
51
+
52
+ ${ui.bold('Webhooks:')}
53
+ webhook register <url>
54
+ webhook test <url>
55
+ webhook history [--page N --pageSize N]
56
+
57
+ ${ui.bold('Global flags:')}
58
+ --json Output raw JSON instead of formatted tables
59
+ --api-key <key> Override saved API key for this run
60
+ --page N Pagination page (default 1)
61
+ --pageSize N Pagination page size (default 10)
62
+ -h, --help Show help
63
+ -v, --version Show version
64
+
65
+ ${ui.dim('Endpoint: https://api.mayar.id (production only)')}
66
+ ${ui.dim('Config: ~/.mayar/config.json (chmod 600)')}
67
+ `;
68
+
69
+ function parseFlags(argv) {
70
+ const flags = {};
71
+ const positional = [];
72
+ for (let i = 0; i < argv.length; i++) {
73
+ const a = argv[i];
74
+ if (a === '--') { positional.push(...argv.slice(i + 1)); break; }
75
+ if (a === '--json') flags.json = true;
76
+ else if (a === '--force') flags.force = true;
77
+ else if (a === '--api-key') flags.apiKey = argv[++i];
78
+ else if (a === '--page') flags.page = argv[++i];
79
+ else if (a === '--pageSize' || a === '--page-size') flags.pageSize = argv[++i];
80
+ else if (a === '--data') flags.data = argv[++i];
81
+ else if (a === '-h' || a === '--help') flags.help = true;
82
+ else if (a === '-v' || a === '--version') flags.version = true;
83
+ else if (a.startsWith('--')) {
84
+ const eq = a.indexOf('=');
85
+ if (eq !== -1) flags[a.slice(2, eq)] = a.slice(eq + 1);
86
+ else flags[a.slice(2)] = argv[++i];
87
+ } else {
88
+ positional.push(a);
89
+ }
90
+ }
91
+ return { flags, positional };
92
+ }
93
+
94
+ async function ensureKey(flags) {
95
+ if (flags.apiKey) return flags.apiKey;
96
+ const cfg = config.load();
97
+ if (cfg && cfg.apiKey) return cfg.apiKey;
98
+ // First-run flow
99
+ ui.printBanner();
100
+ process.stdout.write(`${ui.bold('Welcome to Mayar CLI.')}\n`);
101
+ process.stdout.write(`No API key found. Get yours from ${ui.cyan('https://web.mayar.id')} → Integration → API Key.\n\n`);
102
+ if (!process.stdin.isTTY) {
103
+ process.stderr.write(ui.red('Stdin is not a TTY — cannot prompt. Run `mayar init` interactively or pass --api-key.\n'));
104
+ process.exit(1);
105
+ }
106
+ const key = await ui.askSecret(ui.bold('Paste your production API key: '));
107
+ if (!key.trim()) { process.stderr.write(ui.red('No key provided. Aborting.\n')); process.exit(1); }
108
+ config.save({ apiKey: key.trim(), endpoint: 'production', savedAt: new Date().toISOString() });
109
+ process.stdout.write(ui.green(`✓ Saved to ${config.file}`) + '\n\n');
110
+ return key.trim();
111
+ }
112
+
113
+ async function run(argv) {
114
+ const { flags, positional } = parseFlags(argv);
115
+
116
+ if (flags.version) { process.stdout.write(VERSION + '\n'); return; }
117
+ if (!positional.length || (flags.help && !positional.length)) { process.stdout.write(HELP()); return; }
118
+
119
+ const [cmd, sub, ...rest] = positional;
120
+
121
+ try {
122
+ if (cmd === 'help') { process.stdout.write(HELP()); return; }
123
+ if (cmd === 'init') {
124
+ const init = require('./commands/init');
125
+ return await init.run({ flags });
126
+ }
127
+ if (cmd === 'config') {
128
+ if (sub === 'show') {
129
+ const cfg = config.load();
130
+ if (!cfg) { process.stdout.write(ui.dim('(no config saved)') + '\n'); return; }
131
+ const masked = cfg.apiKey ? cfg.apiKey.slice(0, 6) + '…' + cfg.apiKey.slice(-4) : '(none)';
132
+ process.stdout.write(`Path: ${config.file}\n`);
133
+ process.stdout.write(`API Key: ${masked}\n`);
134
+ process.stdout.write(`Endpoint: ${cfg.endpoint || 'production'}\n`);
135
+ process.stdout.write(`Saved at: ${cfg.savedAt || ''}\n`);
136
+ return;
137
+ }
138
+ if (sub === 'reset') {
139
+ const ok = config.clear();
140
+ process.stdout.write((ok ? ui.green('✓ Config cleared.') : ui.dim('(no config to clear)')) + '\n');
141
+ return;
142
+ }
143
+ process.stdout.write('Usage: mayar config <show|reset>\n');
144
+ return;
145
+ }
146
+
147
+ const handlers = {
148
+ balance: './commands/balance',
149
+ invoice: './commands/invoice',
150
+ product: './commands/product',
151
+ payment: './commands/payment',
152
+ customer: './commands/customer',
153
+ tx: './commands/transaction',
154
+ transaction: './commands/transaction',
155
+ transactions: './commands/transaction',
156
+ qr: './commands/qrcode',
157
+ qrcode: './commands/qrcode',
158
+ webhook: './commands/webhook',
159
+ };
160
+ const handler = handlers[cmd];
161
+ if (!handler) {
162
+ process.stderr.write(ui.red(`Unknown command: ${cmd}`) + '\n\n');
163
+ process.stdout.write(HELP());
164
+ process.exit(1);
165
+ }
166
+
167
+ const apiKey = await ensureKey(flags);
168
+ const ctx = { apiKey, flags, positional: [sub, ...rest].filter((x) => x !== undefined) };
169
+ return await require(handler).run(ctx);
170
+ } catch (err) {
171
+ process.stderr.write(ui.red('Error: ' + (err.message || String(err))) + '\n');
172
+ process.exit(1);
173
+ }
174
+ }
175
+
176
+ module.exports = { run };
@@ -0,0 +1,16 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ async function run({ apiKey, flags }) {
6
+ const res = await api.request('GET', '/hl/v1/balance', { apiKey });
7
+ checkResp(res);
8
+ if (flags.json) return ui.jsonOut(res.body);
9
+ const d = (res.body && res.body.data) || {};
10
+ if (!Object.keys(d).length) return ui.jsonOut(res.body);
11
+ const fmt = (n) => (typeof n === 'number' ? n.toLocaleString('id-ID') : String(n ?? '?'));
12
+ const rows = Object.entries(d).map(([k, v]) => ({ field: k, value: fmt(v) }));
13
+ ui.table(rows, ['field', 'value']);
14
+ }
15
+
16
+ module.exports = { run };
@@ -0,0 +1,34 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp, readData } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar customer <list|create>';
6
+
7
+ async function run({ apiKey, flags, positional }) {
8
+ const [sub] = positional;
9
+ switch (sub) {
10
+ case 'list': {
11
+ const res = await api.request('GET', '/hl/v1/customer', {
12
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
13
+ });
14
+ checkResp(res);
15
+ if (flags.json) return ui.jsonOut(res.body);
16
+ const data = (res.body && res.body.data) || [];
17
+ const rows = data.map((c) => ({
18
+ id: c.id, name: c.name, email: c.email, mobile: c.mobile, status: c.status,
19
+ }));
20
+ ui.table(rows, ['id', 'name', 'email', 'mobile', 'status']);
21
+ return;
22
+ }
23
+ case 'create': {
24
+ const body = readData(flags.data);
25
+ if (!body) throw new Error('mayar customer create requires --data \'{"name":"...","email":"...","mobile":"..."}\'');
26
+ const res = await api.request('POST', '/hl/v1/customer/create', { apiKey, body });
27
+ checkResp(res); ui.jsonOut(res.body); return;
28
+ }
29
+ default:
30
+ throw new Error(USAGE);
31
+ }
32
+ }
33
+
34
+ module.exports = { run };
@@ -0,0 +1,25 @@
1
+ const config = require('../config');
2
+ const ui = require('../ui');
3
+
4
+ async function run({ flags }) {
5
+ ui.printBanner();
6
+ const existing = config.load();
7
+ if (existing && existing.apiKey && !flags.force) {
8
+ const masked = existing.apiKey.slice(0, 6) + '…' + existing.apiKey.slice(-4);
9
+ process.stdout.write(ui.dim(`A key is already configured (${masked}).`) + '\n');
10
+ const ans = await ui.ask('Overwrite? [y/N] ');
11
+ if (!/^y/i.test(ans.trim())) { process.stdout.write('Cancelled.\n'); return; }
12
+ }
13
+ process.stdout.write(`${ui.bold('Welcome to Mayar CLI.')}\n`);
14
+ process.stdout.write(`Get your key from ${ui.cyan('https://web.mayar.id')} → Integration → API Key.\n\n`);
15
+ const key = await ui.askSecret(ui.bold('Paste your production API key: '));
16
+ if (!key.trim()) { process.stderr.write(ui.red('No key provided.\n')); process.exit(1); }
17
+ config.save({ apiKey: key.trim(), endpoint: 'production', savedAt: new Date().toISOString() });
18
+ process.stdout.write(ui.green(`✓ Saved to ${config.file}`) + '\n\n');
19
+ process.stdout.write('Try:\n');
20
+ process.stdout.write(' mayar balance\n');
21
+ process.stdout.write(' mayar invoice list\n');
22
+ process.stdout.write(' mayar product list\n');
23
+ }
24
+
25
+ module.exports = { run };
@@ -0,0 +1,55 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp, readData } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar invoice <list|get|close|reopen|create>';
6
+
7
+ async function run({ apiKey, flags, positional }) {
8
+ const [sub, ...rest] = positional;
9
+ switch (sub) {
10
+ case 'list': {
11
+ const res = await api.request('GET', '/hl/v1/invoice', {
12
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
13
+ });
14
+ checkResp(res);
15
+ if (flags.json) return ui.jsonOut(res.body);
16
+ const data = (res.body && res.body.data) || [];
17
+ const rows = data.map((i) => ({
18
+ id: i.id,
19
+ customer: (i.customer && (i.customer.name || i.customer.email)) || '',
20
+ amount: i.amount,
21
+ status: i.status,
22
+ createdAt: i.createdAt,
23
+ }));
24
+ ui.table(rows, ['id', 'customer', 'amount', 'status', 'createdAt']);
25
+ const m = res.body || {};
26
+ process.stdout.write(ui.dim(`page ${m.page ?? '?'} / ${m.pageCount ?? '?'} · total ${m.total ?? data.length}`) + '\n');
27
+ return;
28
+ }
29
+ case 'get': {
30
+ if (!rest[0]) throw new Error('Usage: mayar invoice get <id>');
31
+ const res = await api.request('GET', `/hl/v1/invoice/${encodeURIComponent(rest[0])}`, { apiKey });
32
+ checkResp(res); ui.jsonOut(res.body); return;
33
+ }
34
+ case 'close': {
35
+ if (!rest[0]) throw new Error('Usage: mayar invoice close <id>');
36
+ const res = await api.request('GET', `/hl/v1/invoice/close/${encodeURIComponent(rest[0])}`, { apiKey });
37
+ checkResp(res); ui.jsonOut(res.body); return;
38
+ }
39
+ case 'reopen': {
40
+ if (!rest[0]) throw new Error('Usage: mayar invoice reopen <id>');
41
+ const res = await api.request('GET', `/hl/v1/invoice/open/${encodeURIComponent(rest[0])}`, { apiKey });
42
+ checkResp(res); ui.jsonOut(res.body); return;
43
+ }
44
+ case 'create': {
45
+ const body = readData(flags.data);
46
+ if (!body) throw new Error('mayar invoice create requires --data <json|@file.json>');
47
+ const res = await api.request('POST', '/hl/v1/invoice/create', { apiKey, body });
48
+ checkResp(res); ui.jsonOut(res.body); return;
49
+ }
50
+ default:
51
+ throw new Error(USAGE);
52
+ }
53
+ }
54
+
55
+ module.exports = { run };
@@ -0,0 +1,49 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp, readData } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar payment <list|get|close|reopen|create>';
6
+
7
+ async function run({ apiKey, flags, positional }) {
8
+ const [sub, ...rest] = positional;
9
+ switch (sub) {
10
+ case 'list': {
11
+ const res = await api.request('GET', '/hl/v1/payment', {
12
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
13
+ });
14
+ checkResp(res);
15
+ if (flags.json) return ui.jsonOut(res.body);
16
+ const data = (res.body && res.body.data) || [];
17
+ const rows = data.map((p) => ({
18
+ id: p.id, name: p.name, amount: p.amount, status: p.status, createdAt: p.createdAt,
19
+ }));
20
+ ui.table(rows, ['id', 'name', 'amount', 'status', 'createdAt']);
21
+ return;
22
+ }
23
+ case 'get': {
24
+ if (!rest[0]) throw new Error('Usage: mayar payment get <id>');
25
+ const res = await api.request('GET', `/hl/v1/payment/${encodeURIComponent(rest[0])}`, { apiKey });
26
+ checkResp(res); ui.jsonOut(res.body); return;
27
+ }
28
+ case 'close': {
29
+ if (!rest[0]) throw new Error('Usage: mayar payment close <id>');
30
+ const res = await api.request('GET', `/hl/v1/payment/close/${encodeURIComponent(rest[0])}`, { apiKey });
31
+ checkResp(res); ui.jsonOut(res.body); return;
32
+ }
33
+ case 'reopen': {
34
+ if (!rest[0]) throw new Error('Usage: mayar payment reopen <id>');
35
+ const res = await api.request('GET', `/hl/v1/payment/open/${encodeURIComponent(rest[0])}`, { apiKey });
36
+ checkResp(res); ui.jsonOut(res.body); return;
37
+ }
38
+ case 'create': {
39
+ const body = readData(flags.data);
40
+ if (!body) throw new Error('mayar payment create requires --data <json|@file.json>');
41
+ const res = await api.request('POST', '/hl/v1/payment/create', { apiKey, body });
42
+ checkResp(res); ui.jsonOut(res.body); return;
43
+ }
44
+ default:
45
+ throw new Error(USAGE);
46
+ }
47
+ }
48
+
49
+ module.exports = { run };
@@ -0,0 +1,73 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar product <list|get|close|reopen|search|type>';
6
+
7
+ function renderList(body) {
8
+ const data = (body && body.data) || [];
9
+ const rows = data.map((p) => ({
10
+ id: p.id,
11
+ name: p.name,
12
+ type: p.type,
13
+ amount: p.amount,
14
+ status: p.status,
15
+ productLink: p.linkUrl || '',
16
+ checkoutLink: p.linkPayment || '',
17
+ }));
18
+ ui.table(rows, ['id', 'name', 'type', 'amount', 'status', 'productLink', 'checkoutLink']);
19
+ const m = body || {};
20
+ process.stdout.write(ui.dim(`page ${m.page ?? '?'} / ${m.pageCount ?? '?'} · total ${m.total ?? data.length}`) + '\n');
21
+ }
22
+
23
+ async function run({ apiKey, flags, positional }) {
24
+ const [sub, ...rest] = positional;
25
+ switch (sub) {
26
+ case 'list': {
27
+ const res = await api.request('GET', '/hl/v1/product', {
28
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
29
+ });
30
+ checkResp(res);
31
+ if (flags.json) return ui.jsonOut(res.body);
32
+ renderList(res.body); return;
33
+ }
34
+ case 'search': {
35
+ if (!rest[0]) throw new Error('Usage: mayar product search <keyword>');
36
+ const search = rest.join(' ');
37
+ const res = await api.request('GET', '/hl/v1/product', {
38
+ apiKey, query: { search, page: flags.page, pageSize: flags.pageSize },
39
+ });
40
+ checkResp(res);
41
+ if (flags.json) return ui.jsonOut(res.body);
42
+ renderList(res.body); return;
43
+ }
44
+ case 'type': {
45
+ if (!rest[0]) throw new Error('Usage: mayar product type <ebook|course|membership|saas|event|webinar|...>');
46
+ const res = await api.request('GET', `/hl/v1/product/type/${encodeURIComponent(rest[0])}`, {
47
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
48
+ });
49
+ checkResp(res);
50
+ if (flags.json) return ui.jsonOut(res.body);
51
+ renderList(res.body); return;
52
+ }
53
+ case 'get': {
54
+ if (!rest[0]) throw new Error('Usage: mayar product get <id>');
55
+ const res = await api.request('GET', `/hl/v1/product/${encodeURIComponent(rest[0])}`, { apiKey });
56
+ checkResp(res); ui.jsonOut(res.body); return;
57
+ }
58
+ case 'close': {
59
+ if (!rest[0]) throw new Error('Usage: mayar product close <id>');
60
+ const res = await api.request('GET', `/hl/v1/product/close/${encodeURIComponent(rest[0])}`, { apiKey });
61
+ checkResp(res); ui.jsonOut(res.body); return;
62
+ }
63
+ case 'reopen': {
64
+ if (!rest[0]) throw new Error('Usage: mayar product reopen <id>');
65
+ const res = await api.request('GET', `/hl/v1/product/open/${encodeURIComponent(rest[0])}`, { apiKey });
66
+ checkResp(res); ui.jsonOut(res.body); return;
67
+ }
68
+ default:
69
+ throw new Error(USAGE);
70
+ }
71
+ }
72
+
73
+ module.exports = { run };
@@ -0,0 +1,19 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ async function run({ apiKey, flags, positional }) {
6
+ const [amountArg] = positional;
7
+ if (!amountArg) throw new Error('Usage: mayar qrcode <amount>');
8
+ const amount = Number(amountArg);
9
+ if (!Number.isFinite(amount) || amount <= 0) throw new Error('Amount must be a positive number.');
10
+ const res = await api.request('POST', '/hl/v1/qrcode/create', { apiKey, body: { amount } });
11
+ checkResp(res);
12
+ if (flags.json) return ui.jsonOut(res.body);
13
+ const d = (res.body && res.body.data) || {};
14
+ process.stdout.write(`${ui.bold('QR amount:')} ${d.amount ?? amount}\n`);
15
+ if (d.url) process.stdout.write(`${ui.bold('QR image:')} ${ui.cyan(d.url)}\n`);
16
+ else ui.jsonOut(res.body);
17
+ }
18
+
19
+ module.exports = { run };
@@ -0,0 +1,45 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar tx <list|unpaid>';
6
+
7
+ function renderTx(body) {
8
+ const data = (body && body.data) || [];
9
+ const rows = data.map((t) => ({
10
+ id: t.id,
11
+ customer: (t.customer && (t.customer.name || t.customer.email)) || t.customerName || '',
12
+ amount: t.amount,
13
+ status: t.status,
14
+ createdAt: t.createdAt,
15
+ }));
16
+ ui.table(rows, ['id', 'customer', 'amount', 'status', 'createdAt']);
17
+ }
18
+
19
+ async function run({ apiKey, flags, positional }) {
20
+ const [sub] = positional;
21
+ switch (sub) {
22
+ case undefined:
23
+ case 'list':
24
+ case 'paid': {
25
+ const res = await api.request('GET', '/hl/v1/transactions', {
26
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
27
+ });
28
+ checkResp(res);
29
+ if (flags.json) return ui.jsonOut(res.body);
30
+ renderTx(res.body); return;
31
+ }
32
+ case 'unpaid': {
33
+ const res = await api.request('GET', '/hl/v1/transactions/unpaid', {
34
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
35
+ });
36
+ checkResp(res);
37
+ if (flags.json) return ui.jsonOut(res.body);
38
+ renderTx(res.body); return;
39
+ }
40
+ default:
41
+ throw new Error(USAGE);
42
+ }
43
+ }
44
+
45
+ module.exports = { run };
@@ -0,0 +1,43 @@
1
+ const api = require('../api');
2
+ const ui = require('../ui');
3
+ const { checkResp } = require('../util');
4
+
5
+ const USAGE = 'Usage: mayar webhook <register|test|history>';
6
+
7
+ async function run({ apiKey, flags, positional }) {
8
+ const [sub, ...rest] = positional;
9
+ switch (sub) {
10
+ case 'register': {
11
+ if (!rest[0]) throw new Error('Usage: mayar webhook register <url>');
12
+ const res = await api.request('GET', '/hl/v1/webhook/register', { apiKey, body: { urlHook: rest[0] } });
13
+ checkResp(res); ui.jsonOut(res.body); return;
14
+ }
15
+ case 'test': {
16
+ if (!rest[0]) throw new Error('Usage: mayar webhook test <url>');
17
+ const res = await api.request('POST', '/hl/v1/webhook/test', { apiKey, body: { urlHook: rest[0] } });
18
+ checkResp(res); ui.jsonOut(res.body); return;
19
+ }
20
+ case 'history': {
21
+ const res = await api.request('GET', '/hl/v1/webhook/history', {
22
+ apiKey, query: { page: flags.page, pageSize: flags.pageSize },
23
+ });
24
+ checkResp(res);
25
+ if (flags.json) return ui.jsonOut(res.body);
26
+ const data = (res.body && res.body.data) || [];
27
+ const rows = data.map((w) => ({
28
+ id: w.id, type: w.type, status: w.status, urlDestination: w.urlDestination, createdAt: w.createdAt,
29
+ }));
30
+ ui.table(rows, ['id', 'type', 'status', 'urlDestination', 'createdAt']);
31
+ return;
32
+ }
33
+ case 'retry': {
34
+ if (!rest[0]) throw new Error('Usage: mayar webhook retry <historyId>');
35
+ const res = await api.request('GET', `/hl/v1/webhook/history/retry/${encodeURIComponent(rest[0])}`, { apiKey });
36
+ checkResp(res); ui.jsonOut(res.body); return;
37
+ }
38
+ default:
39
+ throw new Error(USAGE);
40
+ }
41
+ }
42
+
43
+ module.exports = { run };
package/src/config.js ADDED
@@ -0,0 +1,26 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const dir = path.join(os.homedir(), '.mayar');
6
+ const file = path.join(dir, 'config.json');
7
+
8
+ function load() {
9
+ try {
10
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
11
+ } catch (_) {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function save(data) {
17
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
18
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
19
+ try { fs.chmodSync(file, 0o600); } catch (_) {}
20
+ }
21
+
22
+ function clear() {
23
+ try { fs.unlinkSync(file); return true; } catch (_) { return false; }
24
+ }
25
+
26
+ module.exports = { load, save, clear, file, dir };
package/src/ui.js ADDED
@@ -0,0 +1,104 @@
1
+ const readline = require('readline');
2
+
3
+ const isTTY = !!process.stdout.isTTY;
4
+ const wrap = (n) => (s) => isTTY ? `\x1b[${n}m${s}\x1b[0m` : String(s);
5
+ const dim = wrap('2');
6
+ const bold = wrap('1');
7
+ const red = wrap('31');
8
+ const green = wrap('32');
9
+ const yellow = wrap('33');
10
+ const cyan = wrap('36');
11
+ const magenta = wrap('35');
12
+
13
+ const BANNER = `\x1b[35m
14
+ ███╗ ███╗ █████╗ ██╗ ██╗ █████╗ ██████╗
15
+ ████╗ ████║██╔══██╗╚██╗ ██╔╝██╔══██╗██╔══██╗
16
+ ██╔████╔██║███████║ ╚████╔╝ ███████║██████╔╝
17
+ ██║╚██╔╝██║██╔══██║ ╚██╔╝ ██╔══██║██╔══██╗
18
+ ██║ ╚═╝ ██║██║ ██║ ██║ ██║ ██║██║ ██║
19
+ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝\x1b[0m
20
+ \x1b[36m command line interface · production\x1b[0m
21
+ `;
22
+
23
+ function printBanner() {
24
+ if (isTTY) process.stdout.write(BANNER + '\n');
25
+ else process.stdout.write('Mayar CLI\n\n');
26
+ }
27
+
28
+ function ask(question) {
29
+ return new Promise((resolve) => {
30
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
31
+ rl.question(question, (answer) => { rl.close(); resolve(answer); });
32
+ });
33
+ }
34
+
35
+ function askSecret(question) {
36
+ return new Promise((resolve) => {
37
+ process.stdout.write(question);
38
+ const stdin = process.stdin;
39
+ const wasRaw = stdin.isRaw === true;
40
+ stdin.resume();
41
+ stdin.setEncoding('utf8');
42
+ if (stdin.setRawMode) stdin.setRawMode(true);
43
+ let buf = '';
44
+ const cleanup = () => {
45
+ stdin.removeListener('data', onData);
46
+ if (stdin.setRawMode) stdin.setRawMode(wasRaw);
47
+ stdin.pause();
48
+ };
49
+ const onData = (key) => {
50
+ if (key === '') {
51
+ cleanup();
52
+ process.stdout.write('\n');
53
+ process.exit(130);
54
+ }
55
+ if (key === '\r' || key === '\n') {
56
+ cleanup();
57
+ process.stdout.write('\n');
58
+ resolve(buf);
59
+ return;
60
+ }
61
+ if (key === '' || key === '\b') {
62
+ if (buf.length > 0) {
63
+ buf = buf.slice(0, -1);
64
+ process.stdout.write('\b \b');
65
+ }
66
+ return;
67
+ }
68
+ if (key.charCodeAt(0) < 32) return;
69
+ buf += key;
70
+ process.stdout.write('*');
71
+ };
72
+ stdin.on('data', onData);
73
+ });
74
+ }
75
+
76
+ function jsonOut(obj) {
77
+ process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
78
+ }
79
+
80
+ function table(rows, columns) {
81
+ if (!rows || !rows.length) {
82
+ process.stdout.write(dim('(no rows)') + '\n');
83
+ return;
84
+ }
85
+ const widths = columns.map((col) =>
86
+ Math.min(48, Math.max(col.length, ...rows.map((r) => String(r[col] ?? '').length))),
87
+ );
88
+ const head = columns.map((col, i) => bold(col.padEnd(widths[i]))).join(' ');
89
+ process.stdout.write(head + '\n');
90
+ process.stdout.write(columns.map((_, i) => dim('─'.repeat(widths[i]))).join(' ') + '\n');
91
+ for (const r of rows) {
92
+ const line = columns.map((col, i) => {
93
+ let v = String(r[col] ?? '');
94
+ if (v.length > widths[i]) v = v.slice(0, widths[i] - 1) + '…';
95
+ return v.padEnd(widths[i]);
96
+ }).join(' ');
97
+ process.stdout.write(line + '\n');
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ printBanner, ask, askSecret, jsonOut, table,
103
+ dim, bold, red, green, yellow, cyan, magenta,
104
+ };
package/src/util.js ADDED
@@ -0,0 +1,34 @@
1
+ const fs = require('fs');
2
+ const ui = require('./ui');
3
+
4
+ function checkResp(res) {
5
+ if (res.status >= 200 && res.status < 300) return;
6
+ const msg = (res.body && (res.body.messages || res.body.message)) || res.raw || '';
7
+ const err = new Error(`API ${res.status} — ${msg}`);
8
+ err.status = res.status;
9
+ err.body = res.body;
10
+ throw err;
11
+ }
12
+
13
+ function readData(value) {
14
+ if (!value) return undefined;
15
+ if (value.startsWith('@')) {
16
+ const content = fs.readFileSync(value.slice(1), 'utf8');
17
+ return JSON.parse(content);
18
+ }
19
+ return JSON.parse(value);
20
+ }
21
+
22
+ function maybeJson(flags, body, fallback) {
23
+ if (flags.json) {
24
+ ui.jsonOut(body);
25
+ return true;
26
+ }
27
+ if (typeof fallback === 'function') {
28
+ fallback();
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+
34
+ module.exports = { checkResp, readData, maybeJson };