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 +21 -0
- package/README.md +153 -0
- package/bin/mayar.js +2 -0
- package/package.json +40 -0
- package/src/api.js +46 -0
- package/src/cli.js +176 -0
- package/src/commands/balance.js +16 -0
- package/src/commands/customer.js +34 -0
- package/src/commands/init.js +25 -0
- package/src/commands/invoice.js +55 -0
- package/src/commands/payment.js +49 -0
- package/src/commands/product.js +73 -0
- package/src/commands/qrcode.js +19 -0
- package/src/commands/transaction.js +45 -0
- package/src/commands/webhook.js +43 -0
- package/src/config.js +26 -0
- package/src/ui.js +104 -0
- package/src/util.js +34 -0
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
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 };
|