moralis-cli 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.
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: moralis-cli
3
+ description: Use the local `moralis-cli` project to access Moralis EVM and Solana data through swagger-generated CLI commands. Trigger this skill when a user wants to list available Moralis operations, log in with a Moralis API key, map a blockchain data request to an `operationId`, or run Moralis CLI commands with required path arguments and optional flags. If a user asks for Moralis endpoint data, DO NOT fallback to web search or hand-crafting HTTP requests.
4
+ ---
5
+
6
+ # Moralis CLI
7
+
8
+ ## Overview
9
+
10
+ Use the repo-local `moralis-cli` to execute Moralis API requests from the terminal.
11
+ Prefer running the CLI directly instead of hand-crafting HTTP requests when the user asks for Moralis endpoint data.
12
+
13
+ ## Quick Start
14
+
15
+ Run from the repository root.
16
+
17
+ Use one of these forms:
18
+
19
+ - `moralis-cli ...` (if installed via `npm link`)
20
+ - `node bin/moralis-cli.js ...` (always works inside the repo)
21
+
22
+ List commands:
23
+
24
+ ```bash
25
+ moralis-cli help
26
+ moralis-cli evm help
27
+ moralis-cli solana help
28
+ ```
29
+
30
+ Log in (save API key):
31
+
32
+ ```bash
33
+ moralis-cli login <api_key>
34
+ # or interactive hidden prompt
35
+ moralis-cli login
36
+ ```
37
+
38
+ Run an endpoint command:
39
+
40
+ ```bash
41
+ moralis-cli evm getTransaction <transaction_hash>
42
+ moralis-cli evm getTransaction <transaction_hash> --chain eth
43
+ moralis-cli solana balance mainnet <wallet_address>
44
+ ```
45
+
46
+ Inspect one command's arguments:
47
+
48
+ ```bash
49
+ moralis-cli evm getTransaction --help
50
+ moralis-cli solana balance --help
51
+ ```
52
+
53
+ ## Workflow
54
+
55
+ 1. Run `moralis-cli help` (or `moralis-cli evm help` / `moralis-cli solana help`) to discover available `operationId` commands by namespace.
56
+ 2. Match the user's requested endpoint/action to the correct namespace (`evm` or `solana`) and command name.
57
+ 3. Run `moralis-cli <namespace> <operationId> --help` to see required path arguments and optional flags.
58
+ 4. Ask for missing required values only (for example transaction hash, wallet address, chain).
59
+ 5. Execute the command and return the JSON output (or summarize it if the user prefers).
60
+
61
+ ## Parameter Mapping
62
+
63
+ - Treat Swagger/OpenAPI `path` parameters as positional CLI arguments in path order.
64
+ - Treat non-path parameters (`query`, `header`, `formData`, `body`) as flags: `--name value`.
65
+ - Pass booleans as `--flag` / `--no-flag` when supported.
66
+ - Pass JSON body parameters as a JSON string, for example:
67
+
68
+ ```bash
69
+ moralis-cli evm somePostOp <id> --bodyParam '{"key":"value"}'
70
+ ```
71
+
72
+ ## Troubleshooting
73
+
74
+ - If `moralis-cli` is not found, run `node bin/moralis-cli.js ...` from the repo root or install with `npm link`.
75
+ - If login cannot prompt, use `moralis-cli login <api_key>` (non-interactive).
76
+ - If swagger cannot load, retry when network access is available. The CLI caches each spec (`evm` and `solana`) after a successful fetch.
77
+ - If a command is unknown, refresh with `moralis-cli evm help` or `moralis-cli solana help` and use the exact `operationId`.
78
+
79
+ ## Examples
80
+
81
+ - User asks: "Show me a transaction on Moralis for this hash."
82
+ Run: `moralis-cli evm getTransaction <transaction_hash>`
83
+ - User asks: "Show me a Solana wallet balance on Moralis."
84
+ Run: `moralis-cli solana balance <network> <wallet_address>`
85
+ - User asks: "What commands does this CLI support?"
86
+ Run: `moralis-cli help`
87
+ - User asks: "Log me in to Moralis CLI."
88
+ Run: `moralis-cli login` (interactive) or `moralis-cli login <api_key>`
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Moralis CLI"
3
+ short_description: "Access EVM blockchain data via Moralis CLI"
4
+ default_prompt: "Use $moralis-cli to list operations, help with login, and run the correct moralis-cli command for a Moralis API request."
package/.prettierrc ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "printWidth": 140,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "semi": true,
6
+ "useTabs": false,
7
+ "tabWidth": 2,
8
+ "arrowParens": "avoid"
9
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
4
+ "files.exclude": {
5
+ "**/dist": true,
6
+ "**/lib": true,
7
+ "**/node_modules": true
8
+ }
9
+ }
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Moralis CLI
2
+
3
+ Use the Moralis API via Moralis CLI directly inside your AI agent 🤖 while keeping your API key secure and enabling smooth interaction.
4
+
5
+ ## Getting Started
6
+
7
+ Follow these steps to start using Moralis CLI with your AI agent.
8
+
9
+ ## Getting Started
10
+
11
+ Install:
12
+
13
+ ```bash
14
+ npx -g moralis-cli
15
+ ```
16
+
17
+ Login:
18
+
19
+ ```bash
20
+ moralis-cli login
21
+ ```
22
+
23
+ Enter your Moralis API key when prompted (stored securely).
24
+
25
+ Connect to AI
26
+
27
+ Import the [@moralis-cli](.codex/skills/moralis-cli/SKILL.md) skill into your AI agent and start making Moralis API calls securely.
28
+
29
+ ## Examples
30
+
31
+ Codex:
32
+
33
+ ```bash
34
+ › get my balance on solana chain kXB7FfzdrfZpAZEW3TZcp8a8CwQbsowa6BdfAHZ4gVs $moralis-cli
35
+
36
+ • Using the moralis-cli skill for this directly. I’ll run the Solana balance command on mainnet for that wallet and return the current value.
37
+
38
+ • Ran node bin/moralis-cli.js solana balance mainnet kXB7FfzdrfZpAZEW3TZcp8a8CwQbsowa6BdfAHZ4gVs
39
+ â”” {
40
+ "lamports": "24544198",
41
+ "solana": "0.024544198"
42
+ }
43
+
44
+ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
45
+
46
+ • moralis-cli Solana balance for kXB7FfzdrfZpAZEW3TZcp8a8CwQbsowa6BdfAHZ4gVs (mainnet):
47
+
48
+ - 0.024544198 SOL
49
+ - 24,544,198 lamports
50
+ ```
51
+
52
+ # License
53
+
54
+ MIT
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../src/cli');
4
+
5
+ main().catch(error => {
6
+ const message = error && error.message ? error.message : String(error);
7
+ console.error(`Error: ${message}`);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "moralis-cli",
3
+ "version": "0.1.0",
4
+ "description": "Use Moralis API via Moralis CLI directly inside AI agents with secure API key handling and access to EVM and Solana blockchain data.",
5
+ "bin": {
6
+ "moralis-cli": "bin/moralis-cli.js"
7
+ },
8
+ "type": "commonjs",
9
+ "license": "MIT",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "devDependencies": {
14
+ "prettier": "^3.8.1"
15
+ },
16
+ "scripts": {
17
+ "start": "node bin/moralis-cli.js",
18
+ "help": "node bin/moralis-cli.js help"
19
+ },
20
+ "keywords": [
21
+ "moralis",
22
+ "moralis api",
23
+ "moralis cli",
24
+ "ai integration",
25
+ "ai agent",
26
+ "llm tools",
27
+ "blockchain api",
28
+ "web3 api",
29
+ "evm data",
30
+ "solana data",
31
+ "crypto data",
32
+ "onchain data",
33
+ "developer cli",
34
+ "web3 developer tools"
35
+ ]
36
+ }
package/src/cli.js ADDED
@@ -0,0 +1,1119 @@
1
+ 'use strict';
2
+
3
+ const fsp = require('node:fs/promises');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const CONFIG_DIR = process.env.MORALIS_CLI_CONFIG_DIR || path.join(os.homedir(), '.moralis-cli');
8
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
9
+ const SWAGGER_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
10
+ const DEFAULT_FETCH_RETRY_ATTEMPTS = 3;
11
+ const DEFAULT_FETCH_RETRY_BASE_DELAY_MS = 400;
12
+ const API_TARGETS = Object.freeze({
13
+ evm: {
14
+ namespace: 'evm',
15
+ label: 'EVM',
16
+ swaggerUrl: 'https://deep-index.moralis.io/api-docs-2.2/v2.2/swagger.json',
17
+ prodBaseUrl: 'https://deep-index.moralis.io/api/v2.2/',
18
+ cacheFileName: 'swagger-cache-evm.json'
19
+ },
20
+ solana: {
21
+ namespace: 'solana',
22
+ label: 'Solana',
23
+ swaggerUrl: 'https://solana-gateway.moralis.io/api-json',
24
+ prodBaseUrl: 'https://solana-gateway.moralis.io/',
25
+ cacheFileName: 'swagger-cache-solana.json'
26
+ }
27
+ });
28
+
29
+ async function main() {
30
+ ensureFetchSupport();
31
+
32
+ const args = process.argv.slice(2);
33
+ const command = args[0];
34
+
35
+ if (!command || isHelpToken(command)) {
36
+ await printGlobalHelp(args.slice(1));
37
+ return;
38
+ }
39
+
40
+ if (command === 'login') {
41
+ await login(args.slice(1));
42
+ return;
43
+ }
44
+
45
+ const apiTarget = getApiTarget(command);
46
+ if (apiTarget) {
47
+ await handleApiCommand(apiTarget, args.slice(1));
48
+ return;
49
+ }
50
+
51
+ console.error(`Unknown command: ${command}`);
52
+ console.error('Run `moralis-cli help` to see supported commands.');
53
+ process.exitCode = 1;
54
+ }
55
+
56
+ function ensureFetchSupport() {
57
+ if (typeof fetch !== 'function') {
58
+ throw new Error('This CLI requires Node.js 18+ (global fetch is unavailable).');
59
+ }
60
+ }
61
+
62
+ function isHelpToken(value) {
63
+ return value === 'help' || value === '--help' || value === '-h';
64
+ }
65
+
66
+ function getApiTarget(name) {
67
+ return API_TARGETS[name] || null;
68
+ }
69
+
70
+ async function handleApiCommand(apiTarget, args) {
71
+ const subcommand = args[0];
72
+
73
+ if (!subcommand) {
74
+ await printNamespaceHelp(apiTarget);
75
+ return;
76
+ }
77
+
78
+ if (subcommand === 'help') {
79
+ const maybeOperationId = args[1];
80
+ if (!maybeOperationId || isHelpToken(maybeOperationId)) {
81
+ await printNamespaceHelp(apiTarget);
82
+ return;
83
+ }
84
+
85
+ const spec = await loadSwaggerSpec(apiTarget);
86
+ const operations = buildOperationMap(spec);
87
+ const operation = operations.get(maybeOperationId);
88
+ if (!operation) {
89
+ console.error(`Unknown ${apiTarget.namespace} operation: ${maybeOperationId}`);
90
+ console.error(`Run \`moralis-cli ${apiTarget.namespace} help\` to see supported operations.`);
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+
95
+ printOperationHelp(spec, operation, apiTarget);
96
+ return;
97
+ }
98
+
99
+ if (subcommand === '--help' || subcommand === '-h') {
100
+ await printNamespaceHelp(apiTarget);
101
+ return;
102
+ }
103
+
104
+ const spec = await loadSwaggerSpec(apiTarget);
105
+ const operations = buildOperationMap(spec);
106
+ const operation = operations.get(subcommand);
107
+
108
+ if (!operation) {
109
+ console.error(`Unknown ${apiTarget.namespace} operation: ${subcommand}`);
110
+ console.error(`Run \`moralis-cli ${apiTarget.namespace} help\` to see supported operations.`);
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+
115
+ const commandArgs = args.slice(1);
116
+ if (commandArgs.includes('--help') || commandArgs.includes('-h')) {
117
+ printOperationHelp(spec, operation, apiTarget);
118
+ return;
119
+ }
120
+
121
+ await invokeOperation(apiTarget, spec, operation, commandArgs);
122
+ }
123
+
124
+ async function printGlobalHelp(args = []) {
125
+ const requestedNamespace = args[0];
126
+ if (requestedNamespace) {
127
+ const requestedTarget = getApiTarget(requestedNamespace);
128
+ if (requestedTarget) {
129
+ await printNamespaceHelp(requestedTarget);
130
+ return;
131
+ }
132
+ }
133
+
134
+ console.log('moralis-cli');
135
+ console.log('');
136
+ console.log('Usage:');
137
+ console.log(' moralis-cli help');
138
+ console.log(' moralis-cli help <evm|solana>');
139
+ console.log(' moralis-cli login <api_key>');
140
+ console.log(' moralis-cli login');
141
+ console.log(' moralis-cli evm <operationId> [required-path-args...] [--optional value]');
142
+ console.log(' moralis-cli solana <operationId> [required-path-args...] [--optional value]');
143
+ console.log('');
144
+ console.log('Built-in commands:');
145
+ console.log(' help - Show this help message');
146
+ console.log(' login - Save Moralis API key (prompts if omitted)');
147
+ console.log(' evm - Run EVM API operations');
148
+ console.log(' solana - Run Solana API operations');
149
+ console.log('');
150
+
151
+ const targets = Object.values(API_TARGETS);
152
+ const results = await Promise.allSettled(targets.map(target => loadSwaggerSpec(target)));
153
+
154
+ for (let i = 0; i < targets.length; i += 1) {
155
+ const target = targets[i];
156
+ const result = results[i];
157
+ console.log(`${target.label} operations (${target.namespace}):`);
158
+ if (result.status === 'fulfilled') {
159
+ printOperationList(result.value, target);
160
+ } else {
161
+ console.log(' Unable to load operations right now.');
162
+ console.log(` Reason: ${result.reason?.message || String(result.reason)}`);
163
+ console.log(` Source: ${target.swaggerUrl}`);
164
+ }
165
+ console.log('');
166
+ }
167
+ }
168
+
169
+ async function printNamespaceHelp(apiTarget) {
170
+ console.log(`moralis-cli ${apiTarget.namespace}`);
171
+ console.log('');
172
+ console.log('Usage:');
173
+ console.log(` moralis-cli ${apiTarget.namespace} help`);
174
+ console.log(` moralis-cli ${apiTarget.namespace} <operationId> [required-path-args...] [--optional value]`);
175
+ console.log(` moralis-cli ${apiTarget.namespace} <operationId> --help`);
176
+ console.log('');
177
+ console.log(`Swagger source: ${apiTarget.swaggerUrl}`);
178
+ console.log('');
179
+
180
+ try {
181
+ const spec = await loadSwaggerSpec(apiTarget);
182
+ printOperationList(spec, apiTarget);
183
+ } catch (error) {
184
+ console.log('Unable to load operations right now.');
185
+ console.log(`Reason: ${error.message}`);
186
+ console.log(`Source: ${apiTarget.swaggerUrl}`);
187
+ }
188
+ }
189
+
190
+ function printOperationList(spec, apiTarget) {
191
+ const operationMap = buildOperationMap(spec);
192
+ const operations = Array.from(operationMap.values()).sort((a, b) => a.operationId.localeCompare(b.operationId));
193
+ console.log(`Supported ${apiTarget.label} API operations (${operations.length}):`);
194
+ for (const operation of operations) {
195
+ const example = formatOperationExample(spec, operation, apiTarget);
196
+ const description = formatOperationListDescription(operation);
197
+ console.log(` ${example}${description ? ` - ${description}` : ''}`);
198
+ }
199
+ }
200
+
201
+ async function login(args) {
202
+ let apiKey = args[0];
203
+ if (!apiKey) {
204
+ apiKey = await promptHidden('Enter Moralis API key: ');
205
+ }
206
+
207
+ if (!apiKey || !apiKey.trim()) {
208
+ throw new Error('API key is required.');
209
+ }
210
+
211
+ const config = await readConfig();
212
+ config.apiKey = apiKey.trim();
213
+ await writeConfig(config);
214
+ console.log('API key saved.');
215
+ }
216
+
217
+ function promptHidden(promptText) {
218
+ return new Promise((resolve, reject) => {
219
+ const stdin = process.stdin;
220
+ const stdout = process.stdout;
221
+
222
+ if (!stdin.isTTY || !stdout.isTTY) {
223
+ reject(new Error('Interactive login requires a TTY. Use `moralis-cli login <api_key>`.'));
224
+ return;
225
+ }
226
+
227
+ stdout.write(promptText);
228
+ stdin.resume();
229
+ stdin.setEncoding('utf8');
230
+
231
+ let value = '';
232
+ const wasRaw = stdin.isRaw;
233
+ if (typeof stdin.setRawMode === 'function') {
234
+ stdin.setRawMode(true);
235
+ }
236
+
237
+ const onData = chunk => {
238
+ for (const ch of chunk) {
239
+ if (ch === '\r' || ch === '\n') {
240
+ cleanup();
241
+ stdout.write('\n');
242
+ resolve(value);
243
+ return;
244
+ }
245
+
246
+ if (ch === '\u0003') {
247
+ cleanup();
248
+ reject(new Error('Cancelled.'));
249
+ return;
250
+ }
251
+
252
+ if (ch === '\u007f' || ch === '\b') {
253
+ if (value.length > 0) {
254
+ value = value.slice(0, -1);
255
+ }
256
+ continue;
257
+ }
258
+
259
+ value += ch;
260
+ }
261
+ };
262
+
263
+ const cleanup = () => {
264
+ stdin.off('data', onData);
265
+ if (typeof stdin.setRawMode === 'function') {
266
+ stdin.setRawMode(Boolean(wasRaw));
267
+ }
268
+ stdin.pause();
269
+ };
270
+
271
+ stdin.on('data', onData);
272
+ });
273
+ }
274
+
275
+ async function invokeOperation(apiTarget, spec, operation, argv) {
276
+ const parsedArgs = parseCliArgs(argv);
277
+ const params = normalizeOperationParameters(spec, operation);
278
+ const requiredPathParams = params.filter(p => p.in === 'path').sort(sortPathParamsByPath(operation.path));
279
+
280
+ if (parsedArgs.positionals.length < requiredPathParams.length) {
281
+ printOperationHelp(spec, operation, apiTarget);
282
+ throw new Error(`Missing required path arguments. Expected ${requiredPathParams.length}, got ${parsedArgs.positionals.length}.`);
283
+ }
284
+
285
+ const config = await readConfig();
286
+ const request = buildHttpRequest(apiTarget, spec, operation, params, parsedArgs, config);
287
+ const response = await fetchWithRetry(request.url, request.fetchOptions, {
288
+ attempts: DEFAULT_FETCH_RETRY_ATTEMPTS,
289
+ baseDelayMs: DEFAULT_FETCH_RETRY_BASE_DELAY_MS,
290
+ label: `${operation.method.toUpperCase()} ${operation.operationId}`
291
+ });
292
+ await printResponse(response);
293
+ }
294
+
295
+ function parseCliArgs(argv) {
296
+ const result = {
297
+ positionals: [],
298
+ flags: Object.create(null)
299
+ };
300
+
301
+ for (let i = 0; i < argv.length; i += 1) {
302
+ const token = argv[i];
303
+ if (!token.startsWith('--') || token === '--') {
304
+ result.positionals.push(token);
305
+ continue;
306
+ }
307
+
308
+ if (token.startsWith('--no-')) {
309
+ const key = token.slice(5);
310
+ result.flags[key] = false;
311
+ continue;
312
+ }
313
+
314
+ const eqIndex = token.indexOf('=');
315
+ if (eqIndex >= 0) {
316
+ const key = token.slice(2, eqIndex);
317
+ const value = token.slice(eqIndex + 1);
318
+ result.flags[key] = value;
319
+ continue;
320
+ }
321
+
322
+ const key = token.slice(2);
323
+ const next = argv[i + 1];
324
+ if (next != null && !next.startsWith('--')) {
325
+ result.flags[key] = next;
326
+ i += 1;
327
+ } else {
328
+ result.flags[key] = true;
329
+ }
330
+ }
331
+
332
+ return result;
333
+ }
334
+
335
+ function buildHttpRequest(apiTarget, spec, operation, params, parsedArgs, config) {
336
+ const scheme = pickScheme(spec);
337
+ const base = buildBaseUrl(apiTarget, spec, scheme);
338
+ const pathValueMap = Object.create(null);
339
+ const query = new URLSearchParams();
340
+ const headers = new Headers();
341
+ const consumes = pickConsumes(spec, operation);
342
+ const produces = pickProduces(spec, operation);
343
+ let body;
344
+ let bodyParamSeen = false;
345
+ let bodyContentType;
346
+
347
+ const pathParams = params.filter(p => p.in === 'path').sort(sortPathParamsByPath(operation.path));
348
+ pathParams.forEach((param, index) => {
349
+ const rawValue = parsedArgs.positionals[index];
350
+ if (rawValue == null || rawValue === '') {
351
+ throw new Error(`Missing required path parameter: ${param.name}`);
352
+ }
353
+ pathValueMap[param.name] = coerceParamValue(param, rawValue);
354
+ });
355
+
356
+ for (const param of params) {
357
+ if (param.in === 'path') {
358
+ continue;
359
+ }
360
+
361
+ const flagName = param.name;
362
+ const rawFlagValue = parsedArgs.flags[flagName];
363
+
364
+ if (param.in === 'body') {
365
+ bodyParamSeen = true;
366
+ bodyContentType = param.contentType || bodyContentType;
367
+ if (rawFlagValue == null || rawFlagValue === true) {
368
+ if (param.required) {
369
+ throw new Error(`Missing required body parameter. Pass --${param.name} '<json>'`);
370
+ }
371
+ continue;
372
+ }
373
+ body = parseBodyFlagValue(param, rawFlagValue);
374
+ continue;
375
+ }
376
+
377
+ if (rawFlagValue == null || (rawFlagValue === true && getParameterType(param) !== 'boolean')) {
378
+ if (param.required) {
379
+ throw new Error(`Missing required parameter: --${param.name}`);
380
+ }
381
+ continue;
382
+ }
383
+
384
+ const value = coerceParamValue(param, rawFlagValue);
385
+
386
+ if (param.in === 'query') {
387
+ appendQueryParam(query, param, value);
388
+ continue;
389
+ }
390
+
391
+ if (param.in === 'header') {
392
+ headers.set(param.name, stringifyValue(value));
393
+ continue;
394
+ }
395
+
396
+ if (param.in === 'formData') {
397
+ if (!(body instanceof URLSearchParams)) {
398
+ body = new URLSearchParams();
399
+ }
400
+ appendFormParam(body, param, value);
401
+ continue;
402
+ }
403
+ }
404
+
405
+ let resolvedPath = operation.path;
406
+ for (const [name, value] of Object.entries(pathValueMap)) {
407
+ resolvedPath = resolvedPath.replaceAll(`{${name}}`, encodeURIComponent(stringifyValue(value)));
408
+ }
409
+
410
+ const url = new URL(joinUrl(base, resolvedPath));
411
+ for (const [key, value] of query) {
412
+ url.searchParams.append(key, value);
413
+ }
414
+
415
+ if (produces) {
416
+ headers.set('accept', produces);
417
+ }
418
+
419
+ applyApiKeyAuth(spec, operation, config, headers, url);
420
+
421
+ let requestBody = undefined;
422
+ if (bodyParamSeen && body !== undefined) {
423
+ headers.set('content-type', bodyContentType || consumes || 'application/json');
424
+ requestBody = typeof body === 'string' ? body : JSON.stringify(body);
425
+ } else if (body instanceof URLSearchParams) {
426
+ headers.set('content-type', 'application/x-www-form-urlencoded');
427
+ requestBody = body.toString();
428
+ }
429
+
430
+ return {
431
+ url: url.toString(),
432
+ fetchOptions: {
433
+ method: operation.method.toUpperCase(),
434
+ headers,
435
+ body: allowsBody(operation.method) ? requestBody : undefined
436
+ }
437
+ };
438
+ }
439
+
440
+ function allowsBody(method) {
441
+ const m = method.toLowerCase();
442
+ return m !== 'get' && m !== 'head';
443
+ }
444
+
445
+ function appendQueryParam(query, param, value) {
446
+ if (Array.isArray(value)) {
447
+ if (param.collectionFormat === 'multi' || (param.explode === true && (param.style === 'form' || !param.style))) {
448
+ for (const item of value) {
449
+ query.append(param.name, stringifyValue(item));
450
+ }
451
+ return;
452
+ }
453
+ query.append(param.name, value.map(stringifyValue).join(','));
454
+ return;
455
+ }
456
+ query.append(param.name, stringifyValue(value));
457
+ }
458
+
459
+ function appendFormParam(form, param, value) {
460
+ if (Array.isArray(value)) {
461
+ for (const item of value) {
462
+ form.append(param.name, stringifyValue(item));
463
+ }
464
+ return;
465
+ }
466
+ form.append(param.name, stringifyValue(value));
467
+ }
468
+
469
+ function stringifyValue(value) {
470
+ if (typeof value === 'string') {
471
+ return value;
472
+ }
473
+ if (typeof value === 'number' || typeof value === 'boolean') {
474
+ return String(value);
475
+ }
476
+ return JSON.stringify(value);
477
+ }
478
+
479
+ function parseBodyFlagValue(param, rawValue) {
480
+ if (rawValue === true) {
481
+ throw new Error(`Body parameter --${param.name} requires a JSON value.`);
482
+ }
483
+ try {
484
+ return JSON.parse(String(rawValue));
485
+ } catch {
486
+ throw new Error(`Invalid JSON for --${param.name}`);
487
+ }
488
+ }
489
+
490
+ function getParameterType(param) {
491
+ if (!param || typeof param !== 'object') {
492
+ return undefined;
493
+ }
494
+ return param.type || param.schema?.type;
495
+ }
496
+
497
+ function coerceParamValue(param, rawValue) {
498
+ const paramType = getParameterType(param);
499
+
500
+ if (rawValue === true && paramType === 'boolean') {
501
+ return true;
502
+ }
503
+ if (rawValue === false && paramType === 'boolean') {
504
+ return false;
505
+ }
506
+
507
+ const value = String(rawValue);
508
+
509
+ if (paramType === 'integer') {
510
+ const n = Number.parseInt(value, 10);
511
+ if (Number.isNaN(n)) {
512
+ throw new Error(`Invalid integer for ${param.name}: ${value}`);
513
+ }
514
+ return n;
515
+ }
516
+
517
+ if (paramType === 'number') {
518
+ const n = Number(value);
519
+ if (Number.isNaN(n)) {
520
+ throw new Error(`Invalid number for ${param.name}: ${value}`);
521
+ }
522
+ return n;
523
+ }
524
+
525
+ if (paramType === 'boolean') {
526
+ if (typeof rawValue === 'boolean') {
527
+ return rawValue;
528
+ }
529
+ if (value === 'true') {
530
+ return true;
531
+ }
532
+ if (value === 'false') {
533
+ return false;
534
+ }
535
+ throw new Error(`Invalid boolean for ${param.name}: ${value}`);
536
+ }
537
+
538
+ if (paramType === 'array') {
539
+ return value
540
+ .split(',')
541
+ .map(item => item.trim())
542
+ .filter(Boolean);
543
+ }
544
+
545
+ return value;
546
+ }
547
+
548
+ function applyApiKeyAuth(spec, operation, config, headers, url) {
549
+ if (!config.apiKey) {
550
+ return;
551
+ }
552
+
553
+ const securityDefs = {
554
+ ...(spec.securityDefinitions || {}),
555
+ ...((spec.components && spec.components.securitySchemes) || {})
556
+ };
557
+ const opSecurity = operation.raw.security || spec.security || [];
558
+ const activeSchemes = opSecurity.length ? opSecurity.flatMap(entry => Object.keys(entry)) : Object.keys(securityDefs);
559
+
560
+ const schemeNames = activeSchemes.length ? activeSchemes : Object.keys(securityDefs);
561
+
562
+ for (const schemeName of schemeNames) {
563
+ const scheme = securityDefs[schemeName];
564
+ if (!scheme || scheme.type !== 'apiKey') {
565
+ continue;
566
+ }
567
+
568
+ if (scheme.in === 'header') {
569
+ headers.set(scheme.name, config.apiKey);
570
+ } else if (scheme.in === 'query') {
571
+ url.searchParams.set(scheme.name, config.apiKey);
572
+ }
573
+ }
574
+
575
+ // Fallback for specs that omit apiKey security definitions (Moralis uses X-API-Key).
576
+ if (!headers.has('X-API-Key')) {
577
+ headers.set('X-API-Key', config.apiKey);
578
+ }
579
+ }
580
+
581
+ async function printResponse(response) {
582
+ const contentType = response.headers.get('content-type') || '';
583
+ const isJson = contentType.includes('application/json') || contentType.includes('+json');
584
+ const text = await response.text();
585
+
586
+ if (!response.ok) {
587
+ process.exitCode = 1;
588
+ }
589
+
590
+ if (isJson) {
591
+ try {
592
+ const parsed = JSON.parse(text);
593
+ console.log(JSON.stringify(parsed, null, 2));
594
+ return;
595
+ } catch {
596
+ // Fall back to raw text.
597
+ }
598
+ }
599
+
600
+ if (text) {
601
+ console.log(text);
602
+ } else {
603
+ console.log(`${response.status} ${response.statusText}`);
604
+ }
605
+ }
606
+
607
+ function pickScheme(spec) {
608
+ if (Array.isArray(spec.servers) && spec.servers.length > 0) {
609
+ try {
610
+ const serverUrl = new URL(String(spec.servers[0].url || ''));
611
+ return serverUrl.protocol.replace(':', '') || 'https';
612
+ } catch {
613
+ // Fall back to Swagger 2 fields.
614
+ }
615
+ }
616
+ const schemes = Array.isArray(spec.schemes) ? spec.schemes : [];
617
+ if (schemes.includes('https')) {
618
+ return 'https';
619
+ }
620
+ if (schemes[0]) {
621
+ return schemes[0];
622
+ }
623
+ return 'https';
624
+ }
625
+
626
+ function buildBaseUrl(apiTarget, spec, scheme) {
627
+ if (apiTarget && apiTarget.prodBaseUrl) {
628
+ return apiTarget.prodBaseUrl.replace(/\/+$/, '');
629
+ }
630
+ if (Array.isArray(spec.servers) && spec.servers.length > 0) {
631
+ const serverUrl = spec.servers[0] && spec.servers[0].url;
632
+ if (typeof serverUrl === 'string' && serverUrl.trim()) {
633
+ return serverUrl.replace(/\/+$/, '');
634
+ }
635
+ }
636
+ if (!spec.host) {
637
+ throw new Error('Swagger spec is missing `host`.');
638
+ }
639
+ const basePath = spec.basePath || '/';
640
+ const normalizedBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`;
641
+ return `${scheme}://${spec.host}${normalizedBasePath}`.replace(/\/+$/, '');
642
+ }
643
+
644
+ function joinUrl(base, pathPart) {
645
+ const baseClean = base.replace(/\/+$/, '');
646
+ const pathClean = pathPart.startsWith('/') ? pathPart : `/${pathPart}`;
647
+ return `${baseClean}${pathClean}`;
648
+ }
649
+
650
+ function pickProduces(spec, operation) {
651
+ const responseContentTypes = pickOpenApiResponseContentTypes(operation);
652
+ if (responseContentTypes.length > 0) {
653
+ if (responseContentTypes.includes('application/json')) {
654
+ return 'application/json';
655
+ }
656
+ return responseContentTypes[0];
657
+ }
658
+
659
+ const produces = operation.raw.produces || spec.produces || [];
660
+ if (!Array.isArray(produces) || produces.length === 0) {
661
+ return 'application/json';
662
+ }
663
+ if (produces.includes('application/json')) {
664
+ return 'application/json';
665
+ }
666
+ return produces[0];
667
+ }
668
+
669
+ function pickConsumes(spec, operation) {
670
+ const requestContentTypes = pickOpenApiRequestContentTypes(operation);
671
+ if (requestContentTypes.length > 0) {
672
+ if (requestContentTypes.includes('application/json')) {
673
+ return 'application/json';
674
+ }
675
+ return requestContentTypes[0];
676
+ }
677
+
678
+ const consumes = operation.raw.consumes || spec.consumes || [];
679
+ if (!Array.isArray(consumes) || consumes.length === 0) {
680
+ return 'application/json';
681
+ }
682
+ if (consumes.includes('application/json')) {
683
+ return 'application/json';
684
+ }
685
+ return consumes[0];
686
+ }
687
+
688
+ function buildOperationMap(spec) {
689
+ const map = new Map();
690
+ const paths = spec.paths || {};
691
+
692
+ for (const [apiPath, pathItem] of Object.entries(paths)) {
693
+ for (const method of ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']) {
694
+ const op = pathItem[method];
695
+ if (!op || !op.operationId) {
696
+ continue;
697
+ }
698
+ map.set(op.operationId, {
699
+ operationId: op.operationId,
700
+ method,
701
+ path: apiPath,
702
+ raw: op,
703
+ pathItem
704
+ });
705
+ }
706
+ }
707
+
708
+ return map;
709
+ }
710
+
711
+ function normalizeOperationParameters(spec, operation) {
712
+ const pathParams = Array.isArray(operation.pathItem.parameters) ? operation.pathItem.parameters : [];
713
+ const opParams = Array.isArray(operation.raw.parameters) ? operation.raw.parameters : [];
714
+
715
+ const merged = [];
716
+ const seen = new Set();
717
+
718
+ for (const param of [...pathParams, ...opParams]) {
719
+ const resolved = resolveParameter(spec, param);
720
+ const key = `${resolved.in}:${resolved.name}`;
721
+ if (seen.has(key)) {
722
+ for (let i = 0; i < merged.length; i += 1) {
723
+ const existing = merged[i];
724
+ if (`${existing.in}:${existing.name}` === key) {
725
+ merged[i] = resolved;
726
+ break;
727
+ }
728
+ }
729
+ continue;
730
+ }
731
+ seen.add(key);
732
+ merged.push(resolved);
733
+ }
734
+
735
+ const requestBodyParam = normalizeOpenApiRequestBodyParameter(spec, operation);
736
+ if (requestBodyParam) {
737
+ merged.push(requestBodyParam);
738
+ }
739
+
740
+ return merged;
741
+ }
742
+
743
+ function resolveParameter(spec, param) {
744
+ if (param && param.$ref) {
745
+ return resolveRef(spec, param.$ref);
746
+ }
747
+ return param;
748
+ }
749
+
750
+ function resolveRequestBody(spec, requestBody) {
751
+ if (requestBody && requestBody.$ref) {
752
+ return resolveRef(spec, requestBody.$ref);
753
+ }
754
+ return requestBody;
755
+ }
756
+
757
+ function normalizeOpenApiRequestBodyParameter(spec, operation) {
758
+ const requestBody = resolveRequestBody(spec, operation?.raw?.requestBody);
759
+ if (!requestBody) {
760
+ return null;
761
+ }
762
+
763
+ const content = requestBody.content && typeof requestBody.content === 'object' ? requestBody.content : {};
764
+ const contentTypes = Object.keys(content);
765
+ const preferredContentType = contentTypes.includes('application/json') ? 'application/json' : contentTypes[0] || 'application/json';
766
+ const mediaType = content[preferredContentType] || {};
767
+ const schema = mediaType.schema ? resolveSchema(spec, mediaType.schema) : undefined;
768
+
769
+ return {
770
+ name: 'body',
771
+ in: 'body',
772
+ required: Boolean(requestBody.required),
773
+ type: 'object',
774
+ schema,
775
+ contentType: preferredContentType,
776
+ description: requestBody.description || ''
777
+ };
778
+ }
779
+
780
+ function resolveSchema(spec, schema) {
781
+ if (schema && schema.$ref) {
782
+ return resolveRef(spec, schema.$ref);
783
+ }
784
+ return schema;
785
+ }
786
+
787
+ function resolveRef(root, ref) {
788
+ if (typeof ref !== 'string' || !ref.startsWith('#/')) {
789
+ throw new Error(`Unsupported $ref: ${ref}`);
790
+ }
791
+ const parts = ref.slice(2).split('/').map(unescapeJsonPointer);
792
+ let value = root;
793
+ for (const part of parts) {
794
+ if (value == null || !(part in value)) {
795
+ throw new Error(`Unable to resolve $ref: ${ref}`);
796
+ }
797
+ value = value[part];
798
+ }
799
+ if (value && value.$ref) {
800
+ return resolveRef(root, value.$ref);
801
+ }
802
+ return value;
803
+ }
804
+
805
+ function unescapeJsonPointer(part) {
806
+ return part.replace(/~1/g, '/').replace(/~0/g, '~');
807
+ }
808
+
809
+ function sortPathParamsByPath(apiPath) {
810
+ const order = Array.from(apiPath.matchAll(/\{([^}]+)\}/g)).map(m => m[1]);
811
+ const indexMap = new Map(order.map((name, idx) => [name, idx]));
812
+ return (a, b) => {
813
+ const ai = indexMap.has(a.name) ? indexMap.get(a.name) : Number.MAX_SAFE_INTEGER;
814
+ const bi = indexMap.has(b.name) ? indexMap.get(b.name) : Number.MAX_SAFE_INTEGER;
815
+ return ai - bi;
816
+ };
817
+ }
818
+
819
+ function printOperationHelp(spec, operation, apiTarget) {
820
+ const params = normalizeOperationParameters(spec, operation);
821
+ const pathParams = params.filter(p => p.in === 'path').sort(sortPathParamsByPath(operation.path));
822
+ const otherParams = params.filter(p => p.in !== 'path');
823
+ const usageParts = buildOperationUsageParts(spec, operation, apiTarget);
824
+
825
+ console.log(`Usage: ${usageParts.join(' ')}`);
826
+ console.log('');
827
+ console.log(`${operation.method.toUpperCase()} ${operation.path}`);
828
+ if (otherParams.length > 0) {
829
+ console.log('');
830
+ console.log('Parameters:');
831
+ for (const param of otherParams) {
832
+ const type = getParameterType(param) || (param.in === 'body' ? 'json' : 'string');
833
+ const required = param.required ? 'required' : 'optional';
834
+ console.log(` --${param.name} (${param.in}, ${type}, ${required})`);
835
+ }
836
+ }
837
+ }
838
+
839
+ function buildOperationUsageParts(spec, operation, apiTarget) {
840
+ const params = normalizeOperationParameters(spec, operation);
841
+ const pathParams = params.filter(p => p.in === 'path').sort(sortPathParamsByPath(operation.path));
842
+ const otherParams = params.filter(p => p.in !== 'path');
843
+ const namespace = apiTarget?.namespace || 'evm';
844
+ const usageParts = [`moralis-cli ${namespace} ${operation.operationId}`];
845
+
846
+ for (const param of pathParams) {
847
+ usageParts.push(`<${param.name}>`);
848
+ }
849
+ if (otherParams.length > 0) {
850
+ usageParts.push('[--param value]');
851
+ }
852
+
853
+ return usageParts;
854
+ }
855
+
856
+ function formatOperationExample(spec, operation, apiTarget) {
857
+ const params = normalizeOperationParameters(spec, operation);
858
+ const pathParams = params.filter(p => p.in === 'path').sort(sortPathParamsByPath(operation.path));
859
+ const namespace = apiTarget?.namespace || 'evm';
860
+ const parts = [`moralis-cli ${namespace} ${operation.operationId}`];
861
+ for (const param of pathParams) {
862
+ parts.push(`<${param.name}>`);
863
+ }
864
+ return parts.join(' ');
865
+ }
866
+
867
+ function pickOpenApiRequestContentTypes(operation) {
868
+ const content = operation?.raw?.requestBody?.content;
869
+ if (!content || typeof content !== 'object') {
870
+ return [];
871
+ }
872
+ return Object.keys(content);
873
+ }
874
+
875
+ function pickOpenApiResponseContentTypes(operation) {
876
+ const responses = operation?.raw?.responses;
877
+ if (!responses || typeof responses !== 'object') {
878
+ return [];
879
+ }
880
+
881
+ const preferredStatusCodes = Object.keys(responses).sort((a, b) => {
882
+ const aScore = scoreResponseStatusCode(a);
883
+ const bScore = scoreResponseStatusCode(b);
884
+ return aScore - bScore;
885
+ });
886
+
887
+ for (const statusCode of preferredStatusCodes) {
888
+ const response = responses[statusCode];
889
+ const content = response && response.content;
890
+ if (content && typeof content === 'object') {
891
+ const types = Object.keys(content);
892
+ if (types.length > 0) {
893
+ return types;
894
+ }
895
+ }
896
+ }
897
+
898
+ return [];
899
+ }
900
+
901
+ function scoreResponseStatusCode(statusCode) {
902
+ if (/^2\d\d$/.test(statusCode)) {
903
+ return Number(statusCode);
904
+ }
905
+ if (statusCode === 'default') {
906
+ return 900;
907
+ }
908
+ return 1000;
909
+ }
910
+
911
+ function formatOperationListDescription(operation) {
912
+ const summary = cleanDescriptionText(operation?.raw?.summary);
913
+ if (summary) {
914
+ return truncateText(summary, 90);
915
+ }
916
+
917
+ const description = cleanDescriptionText(operation?.raw?.description);
918
+ if (description) {
919
+ return truncateText(description, 90);
920
+ }
921
+
922
+ return '';
923
+ }
924
+
925
+ function cleanDescriptionText(value) {
926
+ if (typeof value !== 'string') {
927
+ return '';
928
+ }
929
+ return value.replace(/\s+/g, ' ').trim();
930
+ }
931
+
932
+ function truncateText(value, maxLength) {
933
+ if (typeof value !== 'string' || value.length <= maxLength) {
934
+ return value || '';
935
+ }
936
+ return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
937
+ }
938
+
939
+ async function loadSwaggerSpec(apiTarget) {
940
+ if (!apiTarget) {
941
+ throw new Error('API target is required.');
942
+ }
943
+
944
+ const cached = await readSwaggerCache(apiTarget);
945
+ if (cached && Date.now() - cached.fetchedAt < SWAGGER_CACHE_MAX_AGE_MS) {
946
+ return cached.spec;
947
+ }
948
+
949
+ try {
950
+ const response = await fetchWithRetry(
951
+ apiTarget.swaggerUrl,
952
+ { headers: { accept: 'application/json' } },
953
+ {
954
+ attempts: DEFAULT_FETCH_RETRY_ATTEMPTS,
955
+ baseDelayMs: DEFAULT_FETCH_RETRY_BASE_DELAY_MS,
956
+ label: `${apiTarget.namespace} swagger spec`
957
+ }
958
+ );
959
+ if (!response.ok) {
960
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
961
+ }
962
+ const spec = await response.json();
963
+ await writeSwaggerCache(apiTarget, spec);
964
+ return spec;
965
+ } catch (error) {
966
+ if (cached) {
967
+ return cached.spec;
968
+ }
969
+ throw new Error(`Failed to load swagger spec from ${apiTarget.swaggerUrl}: ${error.message}`);
970
+ }
971
+ }
972
+
973
+ async function fetchWithRetry(url, options, retryOptions = {}) {
974
+ const attempts = Math.max(1, Number(retryOptions.attempts || DEFAULT_FETCH_RETRY_ATTEMPTS));
975
+ const baseDelayMs = Math.max(0, Number(retryOptions.baseDelayMs || DEFAULT_FETCH_RETRY_BASE_DELAY_MS));
976
+ const label = retryOptions.label || 'request';
977
+ let lastError;
978
+
979
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
980
+ try {
981
+ const response = await fetch(url, options);
982
+ if (!shouldRetryResponse(response, attempt, attempts)) {
983
+ return response;
984
+ }
985
+
986
+ lastError = new Error(`${label} failed with HTTP ${response.status} ${response.statusText}`);
987
+ await sleepWithBackoff(attempt, baseDelayMs, response);
988
+ continue;
989
+ } catch (error) {
990
+ lastError = error;
991
+ if (attempt >= attempts || !shouldRetryError(error)) {
992
+ throw error;
993
+ }
994
+ await sleepWithBackoff(attempt, baseDelayMs);
995
+ }
996
+ }
997
+
998
+ throw lastError || new Error(`${label} failed`);
999
+ }
1000
+
1001
+ function shouldRetryResponse(response, attempt, maxAttempts) {
1002
+ if (attempt >= maxAttempts) {
1003
+ return false;
1004
+ }
1005
+
1006
+ const status = response.status;
1007
+ return status === 408 || status === 425 || status === 429 || (status >= 500 && status <= 599);
1008
+ }
1009
+
1010
+ function shouldRetryError(error) {
1011
+ if (!error) {
1012
+ return false;
1013
+ }
1014
+
1015
+ // Node fetch throws TypeError for transient network issues (DNS, ECONNRESET, timeouts).
1016
+ return error instanceof TypeError || error.name === 'AbortError';
1017
+ }
1018
+
1019
+ async function sleepWithBackoff(attempt, baseDelayMs, response) {
1020
+ const retryAfterMs = getRetryAfterMs(response);
1021
+ const exponentialDelay = baseDelayMs * Math.pow(2, Math.max(0, attempt - 1));
1022
+ const jitter = Math.floor(Math.random() * Math.max(1, Math.floor(baseDelayMs / 2)));
1023
+ const delayMs = retryAfterMs != null ? Math.max(retryAfterMs, exponentialDelay) : exponentialDelay + jitter;
1024
+ if (delayMs > 0) {
1025
+ await sleep(delayMs);
1026
+ }
1027
+ }
1028
+
1029
+ function getRetryAfterMs(response) {
1030
+ if (!response || typeof response.headers?.get !== 'function') {
1031
+ return null;
1032
+ }
1033
+
1034
+ const retryAfter = response.headers.get('retry-after');
1035
+ if (!retryAfter) {
1036
+ return null;
1037
+ }
1038
+
1039
+ const seconds = Number(retryAfter);
1040
+ if (Number.isFinite(seconds) && seconds >= 0) {
1041
+ return seconds * 1000;
1042
+ }
1043
+
1044
+ const dateMs = Date.parse(retryAfter);
1045
+ if (Number.isNaN(dateMs)) {
1046
+ return null;
1047
+ }
1048
+
1049
+ return Math.max(0, dateMs - Date.now());
1050
+ }
1051
+
1052
+ function sleep(ms) {
1053
+ return new Promise(resolve => setTimeout(resolve, ms));
1054
+ }
1055
+
1056
+ function getSwaggerCachePath(apiTarget) {
1057
+ return path.join(CONFIG_DIR, apiTarget.cacheFileName);
1058
+ }
1059
+
1060
+ async function readSwaggerCache(apiTarget) {
1061
+ const cachePaths = [getSwaggerCachePath(apiTarget)];
1062
+ if (apiTarget.namespace === 'evm') {
1063
+ cachePaths.push(path.join(CONFIG_DIR, 'swagger-cache.json'));
1064
+ }
1065
+
1066
+ try {
1067
+ for (const cachePath of cachePaths) {
1068
+ try {
1069
+ const raw = await fsp.readFile(cachePath, 'utf8');
1070
+ const parsed = JSON.parse(raw);
1071
+ if (parsed && typeof parsed === 'object' && parsed.spec) {
1072
+ return parsed;
1073
+ }
1074
+ } catch {
1075
+ // Try next cache path.
1076
+ }
1077
+ }
1078
+ return null;
1079
+ } catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+
1084
+ async function writeSwaggerCache(apiTarget, spec) {
1085
+ await ensureConfigDir();
1086
+ const payload = { fetchedAt: Date.now(), spec };
1087
+ await fsp.writeFile(getSwaggerCachePath(apiTarget), JSON.stringify(payload), 'utf8');
1088
+ }
1089
+
1090
+ async function readConfig() {
1091
+ try {
1092
+ const raw = await fsp.readFile(CONFIG_PATH, 'utf8');
1093
+ const parsed = JSON.parse(raw);
1094
+ return parsed && typeof parsed === 'object' ? parsed : {};
1095
+ } catch {
1096
+ return {};
1097
+ }
1098
+ }
1099
+
1100
+ async function writeConfig(config) {
1101
+ await ensureConfigDir();
1102
+ await fsp.writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, {
1103
+ encoding: 'utf8',
1104
+ mode: 0o600
1105
+ });
1106
+ try {
1107
+ await fsp.chmod(CONFIG_PATH, 0o600);
1108
+ } catch {
1109
+ // Ignore on filesystems that do not support POSIX perms.
1110
+ }
1111
+ }
1112
+
1113
+ async function ensureConfigDir() {
1114
+ await fsp.mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
1115
+ }
1116
+
1117
+ module.exports = {
1118
+ main
1119
+ };