naracli 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,364 @@
1
+ /**
2
+ * Pool commands
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import { NaraSDK } from "../../client";
7
+ import { DEFAULT_DBC_CONFIG_ADDRESS } from "../../constants";
8
+ import {
9
+ createPool,
10
+ createPoolWithFirstBuy,
11
+ getPoolInfo,
12
+ getPoolProgress,
13
+ } from "../../pool";
14
+ import { loadWallet, getRpcUrl } from "../utils/wallet";
15
+ import {
16
+ validatePublicKey,
17
+ validateRequired,
18
+ validatePositiveNumber,
19
+ validateNonNegativeNumber,
20
+ } from "../utils/validation";
21
+ import {
22
+ handleTransaction,
23
+ printTransactionResult,
24
+ } from "../utils/transaction";
25
+ import { formatOutput, printError, printInfo } from "../utils/output";
26
+ import type {
27
+ PoolCreateOptions,
28
+ PoolCreateWithBuyOptions,
29
+ PoolInfoOptions,
30
+ } from "../types";
31
+
32
+ /**
33
+ * Register pool commands
34
+ * @param program Commander program
35
+ */
36
+ export function registerPoolCommands(program: Command): void {
37
+ const pool = program.command("pool").description("Pool management commands");
38
+
39
+ // pool create
40
+ pool
41
+ .command("create")
42
+ .description("Create a new token pool")
43
+ .requiredOption("-n, --name <string>", "Token name")
44
+ .requiredOption("-s, --symbol <string>", "Token symbol")
45
+ .requiredOption("-u, --uri <string>", "Metadata URI")
46
+ .requiredOption(
47
+ "--dbc-config <address>",
48
+ "DBC config address (or set DBC_CONFIG_ADDRESS env)"
49
+ )
50
+ .option("--creator <address>", "Pool creator address")
51
+ .option("-e, --export-tx", "Export unsigned transaction", false)
52
+ .action(async (options: PoolCreateOptions) => {
53
+ try {
54
+ await handlePoolCreate(options);
55
+ } catch (error: any) {
56
+ printError(error.message);
57
+ process.exit(1);
58
+ }
59
+ });
60
+
61
+ // pool create-with-buy
62
+ pool
63
+ .command("create-with-buy")
64
+ .description("Create a new token pool with initial buy")
65
+ .requiredOption("-n, --name <string>", "Token name")
66
+ .requiredOption("-s, --symbol <string>", "Token symbol")
67
+ .requiredOption("-u, --uri <string>", "Metadata URI")
68
+ .requiredOption(
69
+ "--dbc-config <address>",
70
+ "DBC config address (or set DBC_CONFIG_ADDRESS env)"
71
+ )
72
+ .requiredOption("--amount <number>", "Initial buy amount in NSO")
73
+ .option("--creator <address>", "Pool creator address")
74
+ .option("--buyer <address>", "Buyer address")
75
+ .option("--receiver <address>", "Token receiver address")
76
+ .option("--slippage <number>", "Slippage in basis points", "100")
77
+ .option("-e, --export-tx", "Export unsigned transaction", false)
78
+ .action(async (options: PoolCreateWithBuyOptions) => {
79
+ try {
80
+ await handlePoolCreateWithBuy(options);
81
+ } catch (error: any) {
82
+ printError(error.message);
83
+ process.exit(1);
84
+ }
85
+ });
86
+
87
+ // pool info
88
+ pool
89
+ .command("info <token-address>")
90
+ .description("Get pool information")
91
+ .action(async (tokenAddress: string, options: PoolInfoOptions) => {
92
+ try {
93
+ await handlePoolInfo(tokenAddress, options);
94
+ } catch (error: any) {
95
+ printError(error.message);
96
+ process.exit(1);
97
+ }
98
+ });
99
+
100
+ // pool progress
101
+ pool
102
+ .command("progress <token-address>")
103
+ .description("Get bonding curve progress")
104
+ .action(async (tokenAddress: string, options: PoolInfoOptions) => {
105
+ try {
106
+ await handlePoolProgress(tokenAddress, options);
107
+ } catch (error: any) {
108
+ printError(error.message);
109
+ process.exit(1);
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Handle pool create command
116
+ * @param options Command options
117
+ */
118
+ async function handlePoolCreate(options: PoolCreateOptions): Promise<void> {
119
+ // Load wallet
120
+ const wallet = await loadWallet(options.wallet);
121
+ const rpcUrl = getRpcUrl(options.rpcUrl);
122
+
123
+ printInfo(`Using RPC: ${rpcUrl}`);
124
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
125
+
126
+ // Initialize SDK
127
+ const sdk = new NaraSDK({
128
+ rpcUrl,
129
+ commitment: "confirmed",
130
+ });
131
+
132
+ // Get config address from option or env
133
+ const configAddress =
134
+ options.dbcConfig || DEFAULT_DBC_CONFIG_ADDRESS;
135
+ if (!configAddress) {
136
+ throw new Error(
137
+ "DBC config address is required. Use --dbc-config flag or set DBC_CONFIG_ADDRESS environment variable."
138
+ );
139
+ }
140
+
141
+ // Parse addresses
142
+ const configPubkey = validatePublicKey(configAddress);
143
+ const creator = options.creator
144
+ ? validatePublicKey(options.creator)
145
+ : wallet.publicKey;
146
+
147
+ printInfo("Creating token pool...");
148
+
149
+ // Create pool
150
+ const result = await createPool(sdk, {
151
+ name: options.name,
152
+ symbol: options.symbol,
153
+ uri: options.uri,
154
+ configAddress,
155
+ payer: wallet.publicKey,
156
+ poolCreator: creator,
157
+ });
158
+
159
+ printInfo(`Pool address: ${result.poolAddress}`);
160
+ printInfo(`Token address: ${result.baseMint}`);
161
+
162
+ // Handle transaction
163
+ const txResult = await handleTransaction(
164
+ sdk,
165
+ result.transaction,
166
+ [wallet, result.baseMintKeypair], // Both wallet and baseMint keypair need to sign
167
+ options.exportTx || false
168
+ );
169
+
170
+ // Output result
171
+ if (options.json) {
172
+ const output = {
173
+ poolAddress: result.poolAddress,
174
+ tokenAddress: result.baseMint,
175
+ ...(txResult.signature && { signature: txResult.signature }),
176
+ ...(txResult.base64 && { transaction: txResult.base64 }),
177
+ };
178
+ console.log(JSON.stringify(output, null, 2));
179
+ } else {
180
+ console.log(`\nPool Address: ${result.poolAddress}`);
181
+ console.log(`Token Address: ${result.baseMint}`);
182
+ printTransactionResult(txResult, false);
183
+
184
+ if (txResult.signature) {
185
+ printInfo("\nSave this token address for buying/selling:");
186
+ console.log(`export TOKEN_ADDRESS="${result.baseMint}"`);
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Handle pool create-with-buy command
193
+ * @param options Command options
194
+ */
195
+ async function handlePoolCreateWithBuy(
196
+ options: PoolCreateWithBuyOptions
197
+ ): Promise<void> {
198
+ // Load wallet
199
+ const wallet = await loadWallet(options.wallet);
200
+ const rpcUrl = getRpcUrl(options.rpcUrl);
201
+
202
+ printInfo(`Using RPC: ${rpcUrl}`);
203
+ printInfo(`Wallet: ${wallet.publicKey.toBase58()}`);
204
+
205
+ // Initialize SDK
206
+ const sdk = new NaraSDK({
207
+ rpcUrl,
208
+ commitment: "confirmed",
209
+ });
210
+
211
+ // Get config address from option or env
212
+ const configAddress =
213
+ options.dbcConfig || DEFAULT_DBC_CONFIG_ADDRESS;
214
+ if (!configAddress) {
215
+ throw new Error(
216
+ "DBC config address is required. Use --dbc-config flag or set DBC_CONFIG_ADDRESS environment variable."
217
+ );
218
+ }
219
+
220
+ // Parse addresses
221
+ const configPubkey = validatePublicKey(configAddress);
222
+ const creator = options.creator
223
+ ? validatePublicKey(options.creator)
224
+ : wallet.publicKey;
225
+ const buyer = options.buyer
226
+ ? validatePublicKey(options.buyer)
227
+ : wallet.publicKey;
228
+ const receiver = options.receiver
229
+ ? validatePublicKey(options.receiver)
230
+ : buyer;
231
+
232
+ // Parse numbers
233
+ const amount = validatePositiveNumber(options.amount, "amount");
234
+ const slippage = validateNonNegativeNumber(
235
+ options.slippage || "100",
236
+ "slippage"
237
+ );
238
+
239
+ printInfo("Creating token pool with initial buy...");
240
+ printInfo(`Initial buy amount: ${amount} NSO`);
241
+
242
+ // Create pool with first buy
243
+ const result = await createPoolWithFirstBuy(sdk, {
244
+ name: options.name,
245
+ symbol: options.symbol,
246
+ uri: options.uri,
247
+ configAddress,
248
+ payer: wallet.publicKey,
249
+ poolCreator: creator,
250
+ initialBuyAmountSOL: amount,
251
+ buyer,
252
+ receiver,
253
+ slippageBps: slippage,
254
+ });
255
+
256
+ printInfo(`Pool address: ${result.poolAddress}`);
257
+ printInfo(`Token address: ${result.baseMint}`);
258
+
259
+ // Handle transaction
260
+ const txResult = await handleTransaction(
261
+ sdk,
262
+ result.createPoolTx,
263
+ [wallet, result.baseMintKeypair], // Both wallet and baseMint keypair need to sign
264
+ options.exportTx || false
265
+ );
266
+
267
+ // Output result
268
+ if (options.json) {
269
+ const output = {
270
+ poolAddress: result.poolAddress,
271
+ tokenAddress: result.baseMint,
272
+ buyInfo: result.buyInfo,
273
+ ...(txResult.signature && { signature: txResult.signature }),
274
+ ...(txResult.base64 && { transaction: txResult.base64 }),
275
+ };
276
+ console.log(JSON.stringify(output, null, 2));
277
+ } else {
278
+ console.log(`\nPool Address: ${result.poolAddress}`);
279
+ console.log(`Token Address: ${result.baseMint}`);
280
+ console.log(`\nBuy Info:`);
281
+ console.log(` Amount In: ${(parseInt(result.buyInfo.amountIn) / 1e9).toFixed(4)} NSO`);
282
+ console.log(` Minimum Out: ${result.buyInfo.minimumAmountOut} tokens (smallest unit)`);
283
+ printTransactionResult(txResult, false);
284
+
285
+ if (txResult.signature) {
286
+ printInfo("\nSave this token address for buying/selling:");
287
+ console.log(`export TOKEN_ADDRESS="${result.baseMint}"`);
288
+ }
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Handle pool info command
294
+ * @param tokenAddress Token address
295
+ * @param options Command options
296
+ */
297
+ async function handlePoolInfo(
298
+ tokenAddress: string,
299
+ options: PoolInfoOptions
300
+ ): Promise<void> {
301
+ const rpcUrl = getRpcUrl(options.rpcUrl);
302
+
303
+ printInfo(`Using RPC: ${rpcUrl}`);
304
+
305
+ // Validate address
306
+ validatePublicKey(tokenAddress);
307
+
308
+ // Initialize SDK
309
+ const sdk = new NaraSDK({
310
+ rpcUrl,
311
+ commitment: "confirmed",
312
+ });
313
+
314
+ printInfo("Fetching pool information...");
315
+
316
+ // Get pool info
317
+ const poolInfo = await getPoolInfo(sdk, tokenAddress);
318
+
319
+ // Output result
320
+ formatOutput(poolInfo, options.json || false);
321
+ }
322
+
323
+ /**
324
+ * Handle pool progress command
325
+ * @param tokenAddress Token address
326
+ * @param options Command options
327
+ */
328
+ async function handlePoolProgress(
329
+ tokenAddress: string,
330
+ options: PoolInfoOptions
331
+ ): Promise<void> {
332
+ const rpcUrl = getRpcUrl(options.rpcUrl);
333
+
334
+ printInfo(`Using RPC: ${rpcUrl}`);
335
+
336
+ // Validate address
337
+ validatePublicKey(tokenAddress);
338
+
339
+ // Initialize SDK
340
+ const sdk = new NaraSDK({
341
+ rpcUrl,
342
+ commitment: "confirmed",
343
+ });
344
+
345
+ printInfo("Fetching bonding curve progress...");
346
+
347
+ // Get pool progress
348
+ const progress = await getPoolProgress(sdk, tokenAddress);
349
+
350
+ // Format progress as percentage for human-readable output
351
+ const output = {
352
+ progress: `${(progress.progress * 100).toFixed(2)}%`,
353
+ progressRaw: progress.progress,
354
+ quoteReserve: progress.quoteReserve,
355
+ isMigrated: progress.isMigrated,
356
+ };
357
+
358
+ // Output result
359
+ if (options.json) {
360
+ console.log(JSON.stringify(progress, null, 2));
361
+ } else {
362
+ formatOutput(output, false);
363
+ }
364
+ }
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Quest commands - interact with nara-quest on-chain quiz
3
+ */
4
+
5
+ import { Command } from "commander";
6
+ import { Connection, Keypair } from "@solana/web3.js";
7
+ import { loadWallet, getRpcUrl } from "../utils/wallet";
8
+ import {
9
+ formatOutput,
10
+ printError,
11
+ printInfo,
12
+ printSuccess,
13
+ printWarning,
14
+ } from "../utils/output";
15
+ import type { GlobalOptions } from "../types";
16
+ import { DEFAULT_QUEST_RELAY_URL } from "../../constants";
17
+ import {
18
+ getQuestInfo,
19
+ hasAnswered,
20
+ generateProof,
21
+ submitAnswer,
22
+ submitAnswerViaRelay,
23
+ parseQuestReward,
24
+ } from "../../quest";
25
+
26
+ // ─── Anchor error parsing ────────────────────────────────────────
27
+ const QUEST_ERRORS: Record<number, string> = {
28
+ 6000: "unauthorized",
29
+ 6001: "poolNotActive",
30
+ 6002: "deadlineExpired",
31
+ 6003: "invalidProof",
32
+ 6004: "invalidDeadline",
33
+ 6005: "insufficientReward",
34
+ 6006: "insufficientPoolBalance",
35
+ 6007: "questionTooLong",
36
+ 6008: "alreadyAnswered",
37
+ };
38
+
39
+ function anchorErrorCode(err: any): string {
40
+ const code = err?.error?.errorCode?.code;
41
+ if (code) return code;
42
+ const raw = err?.message ?? JSON.stringify(err) ?? "";
43
+ const m = raw.match(/"Custom":(\d+)/);
44
+ if (m) return QUEST_ERRORS[parseInt(m[1])] ?? "";
45
+ return "";
46
+ }
47
+
48
+ // ─── Helpers ─────────────────────────────────────────────────────
49
+ function formatTimeRemaining(seconds: number): string {
50
+ if (seconds <= 0) return "expired";
51
+ const m = Math.floor(seconds / 60);
52
+ const s = seconds % 60;
53
+ if (m > 0) return `${m}m ${s}s`;
54
+ return `${s}s`;
55
+ }
56
+
57
+ // ─── Command: quest get ──────────────────────────────────────────
58
+ async function handleQuestGet(options: GlobalOptions) {
59
+ const rpcUrl = getRpcUrl(options.rpcUrl);
60
+ const connection = new Connection(rpcUrl, "confirmed");
61
+
62
+ let wallet: Keypair;
63
+ try {
64
+ wallet = await loadWallet(options.wallet);
65
+ } catch {
66
+ wallet = Keypair.generate();
67
+ }
68
+
69
+ let quest;
70
+ try {
71
+ quest = await getQuestInfo(connection, wallet);
72
+ } catch (err: any) {
73
+ printError(`Failed to fetch quest info: ${err.message}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ if (!quest.active) {
78
+ printWarning("No active quest at the moment");
79
+ if (options.json) {
80
+ formatOutput({ active: false }, true);
81
+ }
82
+ return;
83
+ }
84
+
85
+ const data = {
86
+ round: quest.round,
87
+ questionId: quest.questionId,
88
+ question: quest.question,
89
+ rewardPerWinner: `${quest.rewardPerWinner} NSO`,
90
+ totalReward: `${quest.totalReward} NSO`,
91
+ rewardSlots: `${quest.winnerCount}/${quest.rewardCount}`,
92
+ remainingRewardSlots: quest.remainingSlots,
93
+ deadline: new Date(quest.deadline * 1000).toLocaleString(),
94
+ timeRemaining: formatTimeRemaining(quest.timeRemaining),
95
+ expired: quest.expired,
96
+ };
97
+
98
+ if (options.json) {
99
+ formatOutput(data, true);
100
+ } else {
101
+ console.log("");
102
+ console.log(` Question: ${quest.question}`);
103
+ console.log(` Round: #${quest.round}`);
104
+ console.log(` Reward per winner: ${quest.rewardPerWinner} NSO`);
105
+ console.log(` Total reward: ${quest.totalReward} NSO`);
106
+ console.log(
107
+ ` Reward slots: ${quest.winnerCount}/${quest.rewardCount} (${quest.remainingSlots} remaining)`
108
+ );
109
+ console.log(` Deadline: ${new Date(quest.deadline * 1000).toLocaleString()}`);
110
+ if (quest.timeRemaining > 0) {
111
+ console.log(` Time remaining: ${formatTimeRemaining(quest.timeRemaining)}`);
112
+ } else {
113
+ printWarning("Quest has expired");
114
+ }
115
+ console.log("");
116
+ }
117
+ }
118
+
119
+ // ─── Command: quest answer ───────────────────────────────────────
120
+ async function handleQuestAnswer(
121
+ answer: string,
122
+ options: GlobalOptions & { relay?: string }
123
+ ) {
124
+ const rpcUrl = getRpcUrl(options.rpcUrl);
125
+ const connection = new Connection(rpcUrl, "confirmed");
126
+ const wallet = await loadWallet(options.wallet);
127
+
128
+ // 1. Fetch quest info
129
+ let quest;
130
+ try {
131
+ quest = await getQuestInfo(connection, wallet);
132
+ } catch (err: any) {
133
+ printError(`Failed to fetch quest info: ${err.message}`);
134
+ process.exit(1);
135
+ }
136
+
137
+ if (!quest.active) {
138
+ printError("No active quest at the moment");
139
+ process.exit(1);
140
+ }
141
+
142
+ if (quest.expired) {
143
+ printError("Quest has expired");
144
+ process.exit(1);
145
+ }
146
+
147
+ // 2. Check if already answered this round
148
+ const alreadyAnswered = await hasAnswered(connection, wallet);
149
+ if (alreadyAnswered) {
150
+ printWarning("You have already answered this round");
151
+ process.exit(0);
152
+ }
153
+
154
+ // 3. Generate ZK proof
155
+ printInfo("Generating ZK proof...");
156
+
157
+ let proof;
158
+ try {
159
+ proof = await generateProof(answer, quest.answerHash, wallet.publicKey);
160
+ } catch (err: any) {
161
+ if (err.message?.includes("Assert Failed")) {
162
+ printError("Wrong answer");
163
+ } else {
164
+ printError(`ZK proof generation failed: ${err.message}`);
165
+ }
166
+ process.exit(1);
167
+ }
168
+
169
+ // 4. Check deadline again after proof generation
170
+ const nowAfterProof = Math.floor(Date.now() / 1000);
171
+ if (nowAfterProof >= quest.deadline) {
172
+ printError("Quest expired during proof generation");
173
+ process.exit(1);
174
+ }
175
+
176
+ // 5. Submit answer
177
+ if (options.relay) {
178
+ // Relay (gasless) submission
179
+ printInfo("Submitting answer via relay...");
180
+ try {
181
+ const relayResult = await submitAnswerViaRelay(
182
+ options.relay,
183
+ wallet.publicKey,
184
+ proof.hex
185
+ );
186
+ printSuccess("Answer submitted via relay!");
187
+ console.log(` Transaction: ${relayResult.txHash}`);
188
+ await handleReward(connection, relayResult.txHash, options);
189
+ } catch (err: any) {
190
+ printError(`Relay submission failed: ${err.message}`);
191
+ process.exit(1);
192
+ }
193
+ } else {
194
+ // Direct on-chain submission
195
+ printInfo("Submitting answer...");
196
+ try {
197
+ const result = await submitAnswer(connection, wallet, proof.solana);
198
+ printSuccess("Answer submitted!");
199
+ console.log(` Transaction: ${result.signature}`);
200
+ await handleReward(connection, result.signature, options);
201
+ } catch (err: any) {
202
+ handleSubmitError(err);
203
+ }
204
+ }
205
+ }
206
+
207
+ // ─── Parse reward from transaction ───────────────────────────────
208
+ async function handleReward(
209
+ connection: Connection,
210
+ txSignature: string,
211
+ options: GlobalOptions
212
+ ) {
213
+ printInfo("Fetching transaction details...");
214
+
215
+ let reward;
216
+ try {
217
+ reward = await parseQuestReward(connection, txSignature);
218
+ } catch {
219
+ printWarning("Failed to fetch transaction details. Please check manually later.");
220
+ console.log(
221
+ ` https://solscan.io/tx/${txSignature}?cluster=devnet`
222
+ );
223
+ return;
224
+ }
225
+
226
+ if (reward.rewarded) {
227
+ printSuccess(`Congratulations! Reward received: ${reward.rewardNso} NSO (winner ${reward.winner})`);
228
+ if (options.json) {
229
+ formatOutput(
230
+ {
231
+ signature: txSignature,
232
+ rewarded: true,
233
+ rewardLamports: reward.rewardLamports,
234
+ rewardNso: reward.rewardNso,
235
+ winner: reward.winner,
236
+ },
237
+ true
238
+ );
239
+ }
240
+ } else {
241
+ printWarning("Correct answer, but no reward — all reward slots have been claimed");
242
+ if (options.json) {
243
+ formatOutput(
244
+ { signature: txSignature, rewarded: false, rewardLamports: 0 },
245
+ true
246
+ );
247
+ }
248
+ }
249
+ }
250
+
251
+ // ─── Error handling ──────────────────────────────────────────────
252
+ function handleSubmitError(err: any) {
253
+ const errCode = anchorErrorCode(err);
254
+ switch (errCode) {
255
+ case "alreadyAnswered":
256
+ printWarning("You have already answered this round");
257
+ break;
258
+ case "deadlineExpired":
259
+ printError("Quest has expired");
260
+ break;
261
+ case "invalidProof":
262
+ printError("Wrong answer (ZK proof verification failed)");
263
+ break;
264
+ case "poolNotActive":
265
+ printError("No active quest at the moment");
266
+ break;
267
+ default:
268
+ printError(`Failed to submit answer: ${err.message ?? String(err)}`);
269
+ if (err.logs) {
270
+ console.log(" Logs:");
271
+ err.logs.slice(-5).forEach((l: string) => console.log(` ${l}`));
272
+ }
273
+ }
274
+ process.exit(1);
275
+ }
276
+
277
+ // ─── Register commands ───────────────────────────────────────────
278
+ export function registerQuestCommands(program: Command): void {
279
+ const quest = program
280
+ .command("quest")
281
+ .description("Quest commands");
282
+
283
+ // quest get
284
+ quest
285
+ .command("get")
286
+ .description("Get current quest info")
287
+ .action(async (_opts: any, cmd: Command) => {
288
+ try {
289
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
290
+ await handleQuestGet(globalOpts);
291
+ } catch (error: any) {
292
+ printError(error.message);
293
+ process.exit(1);
294
+ }
295
+ });
296
+
297
+ // quest answer
298
+ quest
299
+ .command("answer <answer>")
300
+ .description("Submit an answer")
301
+ .option("--relay [url]", `Submit via relay service, gasless (default: ${DEFAULT_QUEST_RELAY_URL})`)
302
+ .action(async (answer: string, opts: any, cmd: Command) => {
303
+ try {
304
+ const globalOpts = cmd.optsWithGlobals() as GlobalOptions;
305
+ const relayUrl = opts.relay === true ? DEFAULT_QUEST_RELAY_URL : opts.relay;
306
+ await handleQuestAnswer(answer, { ...globalOpts, relay: relayUrl });
307
+ } catch (error: any) {
308
+ printError(error.message);
309
+ process.exit(1);
310
+ }
311
+ });
312
+ }