@paynodelabs/paynode-402-cli 2.5.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,400 @@
1
+ import { PayNodeAgentClient, RequestOptions, ethers } from '@paynodelabs/sdk-js';
2
+ import { join, parse } from 'path';
3
+ import { tmpdir } from 'os';
4
+ import fs from 'fs';
5
+ import { spawn } from 'child_process';
6
+ import {
7
+ getPrivateKey,
8
+ resolveNetwork,
9
+ requireMainnetConfirmation,
10
+ reportError,
11
+ jsonEnvelope,
12
+ withRetry,
13
+ generateTaskId,
14
+ isInlineContent,
15
+ cleanupOldTasks,
16
+ DEFAULT_TASK_DIR,
17
+ DEFAULT_MAX_AGE_SECONDS,
18
+ EXIT_CODES,
19
+ SKILL_VERSION,
20
+ GLOBAL_CONFIG,
21
+ BaseCliOptions,
22
+ maskAddress
23
+ } from '../utils.ts';
24
+
25
+ interface UnifiedRequestOptions extends BaseCliOptions {
26
+ method?: string;
27
+ data?: string;
28
+ header?: string | string[];
29
+ background?: boolean;
30
+ output?: string;
31
+ maxAge?: number;
32
+ taskDir?: string;
33
+ taskId?: string;
34
+ }
35
+
36
+ interface CoreResult {
37
+ result: {
38
+ url: string;
39
+ method: string;
40
+ http_status: number;
41
+ content_type: string;
42
+ body_type: 'json' | 'text' | 'binary';
43
+ network: string;
44
+ data: any;
45
+ duration_ms: number;
46
+ dry_run?: boolean;
47
+ wallet?: string;
48
+ message?: string;
49
+ data_binary?: string;
50
+ data_size?: number;
51
+ };
52
+ binaryBuffer?: Uint8Array;
53
+ contentType: string;
54
+ }
55
+
56
+ // --- Background Launcher ---
57
+ function spawnBackground(url: string, args: string[], options: UnifiedRequestOptions) {
58
+ const taskId = options.taskId || generateTaskId(); // Use existing if re-spawning (though unlikely)
59
+ const taskDir = options.taskDir || DEFAULT_TASK_DIR;
60
+ const maxAge = options.maxAge || DEFAULT_MAX_AGE_SECONDS;
61
+ const outputPath = options.output || join(taskDir, `${taskId}.json`);
62
+ const logPath = join(taskDir, `${taskId}.log`);
63
+
64
+ fs.mkdirSync(taskDir, { recursive: true });
65
+ cleanupOldTasks(taskDir, maxAge);
66
+
67
+ const originalArgs = process.argv.slice(2);
68
+ const flagsToRemove = ['--background', '--json', '--task-id', '--output', '--dry-run', '--max-age', '--task-dir'];
69
+ const childArgs: string[] = [];
70
+
71
+ for (let i = 0; i < originalArgs.length; i++) {
72
+ const arg = originalArgs[i];
73
+ if (flagsToRemove.includes(arg)) {
74
+ // If flag takes an argument, skip both flag and value
75
+ if (['--output', '--task-id', '--max-age', '--task-dir'].includes(arg) && i + 1 < originalArgs.length) {
76
+ i++;
77
+ }
78
+ continue;
79
+ }
80
+ childArgs.push(arg);
81
+ }
82
+ childArgs.push('--task-id', taskId, '--output', outputPath);
83
+
84
+ // [SECURITY & LOGIC]
85
+ // This is a self-re-execution for background processing.
86
+ // 1. We spawn the same script (process.argv[1]) with filtered arguments.
87
+ // 2. We add '--task-id' which signals the next execution to use 'executeAndWrite' path.
88
+ // 3. This avoids infinite recursion because the sub-process will NOT have '--background' in its args.
89
+ // 4. Stderr is piped to a .log file to allow debugging of background failures.
90
+
91
+ const logFd = fs.openSync(logPath, 'a');
92
+ // The child command is pinned to 'process.execPath' (the current runtime) and 'process.argv[1]' (the current script).
93
+ // Arguments are filtered to prevent recursive loops.
94
+ // [SECURITY] Filter environment variables passed to background child process.
95
+ // Minimizes exposure of non-essential credentials.
96
+ const whitelist = [
97
+ 'CLIENT_PRIVATE_KEY',
98
+ 'CUSTOM_ROUTER_ADDRESS',
99
+ 'CUSTOM_USDC_ADDRESS',
100
+ 'RPC_URL',
101
+ 'ALCHEMY_API_KEY',
102
+ 'INFURA_API_KEY',
103
+ 'ETHERSCAN_API_KEY',
104
+ 'HTTP_PROXY',
105
+ 'HTTPS_PROXY',
106
+ 'NODE_PATH',
107
+ 'NVM_DIR',
108
+ 'BUN_INSTALL'
109
+ ];
110
+ // Essential OS-level vars
111
+ const baseEnv = Object.fromEntries(
112
+ Object.entries({
113
+ PATH: process.env.PATH,
114
+ HOME: process.env.HOME,
115
+ TMPDIR: process.env.TMPDIR,
116
+ USER: process.env.USER,
117
+ SHELL: process.env.SHELL
118
+ }).filter(([, v]) => v !== undefined)
119
+ ) as Record<string, string>;
120
+ const childEnv: Record<string, string | undefined> = { ...baseEnv };
121
+ for (const key of whitelist) {
122
+ if (process.env[key]) childEnv[key] = process.env[key];
123
+ }
124
+
125
+ const child = spawn(process.execPath, [process.argv[1], ...childArgs], {
126
+ detached: true,
127
+ stdio: ['ignore', 'ignore', logFd],
128
+ env: childEnv
129
+ });
130
+ child.unref();
131
+
132
+ // After unref. the parent no longer needs logFd. The child has its own copy.
133
+ fs.closeSync(logFd);
134
+
135
+ const pendingInfo = {
136
+ status: 'pending',
137
+ task_id: taskId,
138
+ output_file: outputPath,
139
+ task_dir: taskDir,
140
+ max_age_seconds: maxAge,
141
+ command: `cat ${outputPath}`,
142
+ message: 'šŸ•’ x402 background request started. The wallet will automatically handle payments.'
143
+ };
144
+
145
+ if (options.json) {
146
+ console.log(jsonEnvelope(pendingInfo));
147
+ } else {
148
+ console.log(`\nšŸš€ **Background Task Started**`);
149
+ console.log(`- **Task ID**: \`${taskId}\``);
150
+ console.log(`- **Output**: \`${outputPath}\``);
151
+ console.log(`- **Log**: \`${logPath}\``);
152
+ console.log(`\nUse \`cat ${outputPath}\` to check progress or \`tail -f ${logPath}\` for logs.`);
153
+ }
154
+ process.exit(0);
155
+ }
156
+
157
+ // --- Core x402 Execution ---
158
+ async function executeCore(url: string, args: string[], options: UnifiedRequestOptions): Promise<CoreResult> {
159
+ const isJson = !!options.json || !!options.taskId;
160
+ const startTs = Date.now();
161
+
162
+ // [INPUT-GUARD] Validate URL structure
163
+ if (!url || typeof url !== 'string' || (!url.startsWith('http://') && !url.startsWith('https://'))) {
164
+ throw new Error(`Invalid destination URL: '${url}'. Must start with 'http://' or 'https://'.`);
165
+ }
166
+
167
+ const { rpcUrls, networkName, isSandbox } = await resolveNetwork(options.rpc, options.network, options.rpcTimeout);
168
+ requireMainnetConfirmation(isSandbox, !!options.confirmMainnet, isJson);
169
+
170
+ // Handle params (k=v)
171
+ const kvParams: Record<string, string> = {};
172
+ for (const p of args) {
173
+ if (!p.includes('=')) continue;
174
+ const [k, ...v] = p.split('=');
175
+ kvParams[k.trim()] = v.join('=').trim();
176
+ }
177
+
178
+ const method = options.method?.toUpperCase() || (options.data || Object.keys(kvParams).length > 0 ? 'POST' : 'GET');
179
+
180
+ // Headers parsing
181
+ const headers: Record<string, string> = {};
182
+ if (options.header) {
183
+ const headerArray = Array.isArray(options.header) ? options.header : [options.header];
184
+ for (const h of headerArray) {
185
+ if (!h || !h.includes(':')) continue;
186
+ const [k, ...v] = h.split(':');
187
+ headers[k.trim()] = v.join(':').trim();
188
+ }
189
+ }
190
+ // [P1] Inject network header for Proxy validation
191
+ const paynodeNetwork = isSandbox ? 'testnet' : 'mainnet';
192
+ if (!headers['X-PayNode-Network']) {
193
+ headers['X-PayNode-Network'] = paynodeNetwork;
194
+ }
195
+
196
+ // Auto-sniff JSON body for manual data
197
+ if (options.data && !headers['Content-Type'] && !headers['content-type']) {
198
+ try {
199
+ JSON.parse(options.data);
200
+ headers['Content-Type'] = 'application/json';
201
+ } catch { /* ignore */ }
202
+ }
203
+
204
+ const requestOptions: RequestOptions = { method, headers };
205
+ let targetUrl = url;
206
+
207
+ if (method === 'GET') {
208
+ const urlObj = new URL(url);
209
+ for (const [k, v] of Object.entries(kvParams)) {
210
+ urlObj.searchParams.set(k, v);
211
+ }
212
+ targetUrl = urlObj.toString();
213
+ } else {
214
+ if (options.data) {
215
+ requestOptions.body = options.data;
216
+ } else {
217
+ // [Smart Promotion] For POST/PUT, if no explicit body data is given but
218
+ // query parameters exist (either in URL or as args), put them into JSON body.
219
+ const urlObj = new URL(url);
220
+ const combinedParams = { ...kvParams };
221
+
222
+ // If the user only passed the URL with query params (no extra args)
223
+ if (Object.keys(combinedParams).length === 0 && urlObj.searchParams.size > 0) {
224
+ for (const [k, v] of urlObj.searchParams.entries()) {
225
+ combinedParams[k] = v;
226
+ }
227
+ }
228
+
229
+ if (Object.keys(combinedParams).length > 0) {
230
+ requestOptions.json = combinedParams;
231
+ }
232
+ }
233
+ }
234
+
235
+ // Dry-run
236
+ if (options.dryRun) {
237
+ const pkForAddress = GLOBAL_CONFIG.PRIVATE_KEY;
238
+ let walletAddr: string | undefined;
239
+ try {
240
+ if (pkForAddress && isJson) {
241
+ walletAddr = maskAddress((new ethers.Wallet(pkForAddress)).address);
242
+ }
243
+ } catch { /* skip if PK invalid */ }
244
+
245
+ return {
246
+ result: {
247
+ url: targetUrl,
248
+ method,
249
+ http_status: 0,
250
+ content_type: 'application/json',
251
+ body_type: 'json',
252
+ network: networkName,
253
+ data: null,
254
+ duration_ms: 0,
255
+ dry_run: true,
256
+ wallet: walletAddr,
257
+ message: 'Dry-run: request prepared but not sent.'
258
+ },
259
+ contentType: 'application/json'
260
+ };
261
+ }
262
+
263
+ const pk = getPrivateKey(isJson);
264
+
265
+ const client = new PayNodeAgentClient(pk, rpcUrls);
266
+ const response = await withRetry(
267
+ () => client.requestGate(targetUrl, requestOptions),
268
+ 'x402:requestGate'
269
+ );
270
+
271
+ const contentType = response.headers.get('content-type') || 'application/octet-stream';
272
+ const httpStatus = response.status;
273
+ let resultBody: any;
274
+ let bodyType: 'json' | 'text' | 'binary' = 'text';
275
+ let binaryBuffer: Uint8Array | undefined;
276
+
277
+ if (isInlineContent(contentType)) {
278
+ if (contentType.toLowerCase().includes('application/json')) {
279
+ resultBody = await response.json();
280
+ bodyType = 'json';
281
+ } else {
282
+ resultBody = await response.text();
283
+ bodyType = 'text';
284
+ }
285
+ } else {
286
+ const arrayBuf = await response.arrayBuffer();
287
+ binaryBuffer = new Uint8Array(arrayBuf);
288
+ bodyType = 'binary';
289
+ resultBody = null;
290
+ }
291
+
292
+ return {
293
+ result: {
294
+ url: targetUrl,
295
+ method,
296
+ http_status: httpStatus,
297
+ content_type: contentType,
298
+ body_type: bodyType,
299
+ network: networkName,
300
+ data: resultBody,
301
+ duration_ms: Date.now() - startTs
302
+ },
303
+ binaryBuffer,
304
+ contentType
305
+ };
306
+ }
307
+
308
+ // --- Persistence ---
309
+ async function executeAndWrite(url: string, args: string[], options: UnifiedRequestOptions) {
310
+ const taskId = options.taskId || generateTaskId();
311
+ const taskDir = options.taskDir || DEFAULT_TASK_DIR;
312
+ const outputPath = options.output || join(taskDir, `${taskId}.json`);
313
+
314
+ fs.mkdirSync(taskDir, { recursive: true });
315
+
316
+ try {
317
+ const { result, binaryBuffer, contentType } = await executeCore(url, args, options);
318
+
319
+ if (binaryBuffer) {
320
+ const { dir, name } = parse(outputPath);
321
+ const binaryPath = join(dir, `${name}.bin`);
322
+ fs.writeFileSync(binaryPath, binaryBuffer);
323
+ result.data = `[binary: ${contentType}, ${binaryBuffer.length} bytes → ${binaryPath}]`;
324
+ result.data_binary = binaryPath;
325
+ result.data_size = binaryBuffer.length;
326
+ }
327
+
328
+ const finalOutput = {
329
+ version: SKILL_VERSION,
330
+ status: 'completed',
331
+ task_id: taskId,
332
+ ...result,
333
+ completed_at: new Date().toISOString()
334
+ };
335
+
336
+ fs.writeFileSync(outputPath, JSON.stringify(finalOutput, null, 2));
337
+ } catch (error: any) {
338
+ const errorResult = {
339
+ version: SKILL_VERSION,
340
+ status: 'failed',
341
+ task_id: taskId,
342
+ error: error.message,
343
+ errorCode: error?.code || 'internal_error',
344
+ completed_at: new Date().toISOString()
345
+ };
346
+ fs.writeFileSync(outputPath, JSON.stringify(errorResult, null, 2));
347
+ }
348
+ }
349
+
350
+ // --- Main Entry ---
351
+ export async function requestAction(url: string, args: string[], options: UnifiedRequestOptions) {
352
+ if (options.background) {
353
+ spawnBackground(url, args, options);
354
+ return;
355
+ }
356
+
357
+ if (options.taskId) {
358
+ await executeAndWrite(url, args, options);
359
+ return;
360
+ }
361
+
362
+ const isJson = !!options.json;
363
+
364
+ try {
365
+ if (!isJson && !options.dryRun) {
366
+ console.error(`🌐 x402 Request: ${url}...`);
367
+ }
368
+
369
+ const { result, binaryBuffer, contentType } = await executeCore(url, args, options);
370
+
371
+ if (binaryBuffer) {
372
+ const binPath = options.output
373
+ ? join(parse(options.output).dir, `${parse(options.output).name}.bin`)
374
+ : join(tmpdir(), `paynode-${Date.now().toString(36)}.bin`);
375
+
376
+ fs.writeFileSync(binPath, binaryBuffer);
377
+ result.data = `[binary: ${contentType}, ${binaryBuffer.length} bytes → ${binPath}]`;
378
+ result.data_binary = binPath;
379
+ result.data_size = binaryBuffer.length;
380
+ }
381
+
382
+ if (isJson) {
383
+ console.log(jsonEnvelope({ status: 'success', ...result }));
384
+ } else {
385
+ if (result.dry_run) {
386
+ console.log('🧪 DRY RUN PREPARED:');
387
+ console.log(JSON.stringify(result, null, 2));
388
+ } else {
389
+ if (typeof result.data === 'object') {
390
+ console.log(JSON.stringify(result.data, null, 2));
391
+ } else {
392
+ console.log(result.data);
393
+ }
394
+ }
395
+ }
396
+ } catch (error: any) {
397
+ reportError(error, isJson, EXIT_CODES.NETWORK_ERROR);
398
+ }
399
+ }
400
+
@@ -0,0 +1,74 @@
1
+ import fs from 'fs';
2
+ import { join } from 'path';
3
+ import { DEFAULT_TASK_DIR, jsonEnvelope, BaseCliOptions, cleanupOldTasks } from '../utils.ts';
4
+
5
+ interface TasksOptions extends BaseCliOptions {
6
+ clean?: boolean;
7
+ }
8
+
9
+ export async function tasksAction(subcommand: string | undefined, options: TasksOptions) {
10
+ const isJson = !!options.json;
11
+ const taskDir = DEFAULT_TASK_DIR;
12
+
13
+ if (subcommand === 'clean' || options.clean) {
14
+ const cleaned = cleanupOldTasks(taskDir, 0); // Cleanup everything immediately if explicit
15
+ if (isJson) {
16
+ console.log(jsonEnvelope({ status: 'success', message: `Cleaned ${cleaned} task files from ${taskDir}` }));
17
+ } else {
18
+ console.log(`āœ… Successfully cleaned ${cleaned} tasks.`);
19
+ }
20
+ return;
21
+ }
22
+
23
+ // Default is list
24
+ if (!fs.existsSync(taskDir)) {
25
+ if (isJson) {
26
+ console.log(jsonEnvelope({ status: 'success', tasks: [] }));
27
+ } else {
28
+ console.log('No tasks found (Directory does not exist).');
29
+ }
30
+ return;
31
+ }
32
+
33
+ const files = fs.readdirSync(taskDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
34
+ const tasks = files.map(file => {
35
+ try {
36
+ const content = fs.readFileSync(join(taskDir, file), 'utf-8');
37
+ const data = JSON.parse(content);
38
+ const stats = fs.statSync(join(taskDir, file));
39
+ return {
40
+ id: data.task_id || file.replace('.json', ''),
41
+ status: data.status,
42
+ url: data.url,
43
+ method: data.method,
44
+ created_at: stats.birthtime.toISOString(),
45
+ completed_at: data.completed_at,
46
+ error: data.error
47
+ };
48
+ } catch {
49
+ return null;
50
+ }
51
+ }).filter(Boolean).sort((a: any, b: any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
52
+
53
+ if (isJson) {
54
+ console.log(jsonEnvelope({ status: 'success', total: tasks.length, tasks }));
55
+ } else {
56
+ if (tasks.length === 0) {
57
+ console.log(`No tasks found in ${taskDir}`);
58
+ return;
59
+ }
60
+
61
+ console.log(`\nšŸ“‹ Recent x402 Background Tasks in ${taskDir}:`);
62
+ console.log(`──────────────────────────────────────────────────`);
63
+
64
+ for (const t of tasks as any[]) {
65
+ const statusIcon = t.status === 'completed' ? 'āœ…' : t.status === 'failed' ? 'āŒ' : 'šŸ•’';
66
+ const indicator = `(${t.status || 'unknown'})`.padEnd(12);
67
+ const urlPart = t.url ? `| ${t.url}` : '';
68
+ console.log(`${statusIcon} ${t.id.padEnd(12)} ${indicator} ${urlPart}`);
69
+ if (t.error) console.log(` └─ Error: ${t.error}`);
70
+ }
71
+ console.log(`──────────────────────────────────────────────────`);
72
+ console.log(`šŸ’” Usage: 'cat ${taskDir}/<id>.json' for full results.`);
73
+ }
74
+ }
package/index.ts ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bun
2
+ import { cac } from 'cac';
3
+ import pkg from './package.json';
4
+ import { checkAction } from './commands/check.ts';
5
+ import { mintAction } from './commands/mint.ts';
6
+ import { requestAction } from './commands/request.ts';
7
+ import { listPaidApisAction } from './commands/list-paid-apis.ts';
8
+ import { getApiDetailAction } from './commands/get-api-detail.ts';
9
+ import { invokePaidApiAction } from './commands/invoke-paid-api.ts';
10
+ import { tasksAction } from './commands/tasks.ts';
11
+ import { EXIT_CODES } from './utils.ts';
12
+
13
+ const cli = cac('paynode-402');
14
+
15
+ // Global Options
16
+ cli.option('--json', 'Output results in JSON format');
17
+ cli.option('--network <name>', 'Network to use: mainnet or testnet/sepolia');
18
+ cli.option('--rpc <url>', 'Custom RPC URL');
19
+ cli.option('--rpc-timeout <ms>', 'Custom RPC timeout in milliseconds (default: 15000)');
20
+ cli.option('--confirm-mainnet', 'Required flag for mainnet operations (real USDC)');
21
+ cli.option('--dry-run', 'Show request details without sending');
22
+ cli.option('--market-url <url>', 'Marketplace base URL');
23
+
24
+ // Command: check
25
+ cli
26
+ .command('check', 'Check wallet balance (ETH and USDC) on Base L2')
27
+ .action((options) => {
28
+ return checkAction(options);
29
+ });
30
+
31
+ // Command: mint
32
+ cli
33
+ .command('mint', 'Mint USDC on Base Sepolia')
34
+ .option('--amount <amount>', 'Amount to mint (default: 1000)')
35
+ .action((options) => {
36
+ return mintAction(options);
37
+ });
38
+
39
+ // Command: request
40
+ cli
41
+ .command('request <url> [...params]', 'Access protected API and handle x402 payments. Params: key=value pairs for query/body.')
42
+ .option('-X, --method <method>', 'HTTP method (GET, POST, etc.)')
43
+ .option('-d, --data <data>', 'Raw request body data')
44
+ .option('-H, --header [header]', 'HTTP header in "Key: Value" format (can be used multiple times)', { default: [] })
45
+ .option('--background', 'Execute in background, return immediately (AI-friendly)')
46
+ .option('--output <path>', 'Output file path for result (used with --background)')
47
+ .option('--max-age <seconds>', 'Auto-delete task files older than N seconds (default: 3600)')
48
+ .option('--task-dir <path>', 'Task directory for background results (default: <TMPDIR>/paynode-tasks)')
49
+ .option('--task-id <id>', 'Internal: task ID for background worker')
50
+ .action((url, params, options) => {
51
+ return requestAction(url, params, options);
52
+ });
53
+
54
+ // Command: list-paid-apis
55
+ cli
56
+ .command('list-paid-apis', 'List paid APIs from the marketplace catalog')
57
+ .option('--limit <n>', 'Maximum number of APIs to return')
58
+ .option('--tag [tag]', 'Catalog tag filter (can be used multiple times)', { default: [] })
59
+ .option('--seller <seller>', 'Seller identifier filter')
60
+ .action((options) => {
61
+ return listPaidApisAction(options);
62
+ });
63
+
64
+ // Command: get-api-detail
65
+ cli
66
+ .command('get-api-detail <apiId>', 'Get full detail for one paid API')
67
+ .action((apiId, options) => {
68
+ return getApiDetailAction(apiId, options);
69
+ });
70
+
71
+ // Command: invoke-paid-api
72
+ cli
73
+ .command('invoke-paid-api <apiId>', 'Invoke one paid API through the marketplace flow')
74
+ .option('-X, --method <method>', 'HTTP method override')
75
+ .option('-d, --data <data>', 'Invocation payload as raw JSON string')
76
+ .option('-H, --header [header]', 'HTTP header in "Key: Value" format (can be used multiple times)', { default: [] })
77
+ .option('--background', 'Execute in background, return immediately (AI-friendly)')
78
+ .option('--output <path>', 'Output file path for result (used with --background)')
79
+ .option('--max-age <seconds>', 'Auto-delete task files older than N seconds (default: 3600)')
80
+ .option('--task-dir <path>', 'Task directory for background results (default: <TMPDIR>/paynode-tasks)')
81
+ .option('--task-id <id>', 'Internal: task ID for background worker')
82
+ .action((apiId, options) => {
83
+ return invokePaidApiAction(apiId, options);
84
+ });
85
+
86
+ // Command: tasks
87
+ cli
88
+ .command('tasks [subcommand]', 'Manage background tasks (subcommands: list, clean)')
89
+ .option('--clean', 'Clean all task files immediately')
90
+ .action((subcommand, options) => {
91
+ return tasksAction(subcommand, options);
92
+ });
93
+
94
+
95
+ cli.help();
96
+ cli.version(pkg.version);
97
+
98
+ try {
99
+ const result = cli.parse();
100
+ if (result instanceof Promise) {
101
+ result.catch((err) => {
102
+ console.error(`āŒ Global Error: ${err.message}`);
103
+ process.exit(EXIT_CODES.GENERIC_ERROR);
104
+ });
105
+ }
106
+ } catch (error: any) {
107
+ if (error.name === 'CACError') {
108
+ console.error(`āŒ Command Error: ${error.message}`);
109
+ process.exit(EXIT_CODES.INVALID_ARGS);
110
+ } else {
111
+ console.error(`āŒ Parse Error: ${error.message}`);
112
+ process.exit(EXIT_CODES.GENERIC_ERROR);
113
+ }
114
+ }