@descix/cli 1.0.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.
Files changed (94) hide show
  1. package/README.md +95 -0
  2. package/SERVICE_README_sdk.md +399 -0
  3. package/agent-assets/guides/README.md +61 -0
  4. package/agent-assets/guides/dynamic-site-pattern.md +125 -0
  5. package/agent-assets/guides/knowledge-only-pattern.md +93 -0
  6. package/agent-assets/guides/microservice-pattern.md +152 -0
  7. package/agent-assets/guides/static-site-pattern.md +111 -0
  8. package/agent-assets/instructions/app-asset-validation.md +477 -0
  9. package/agent-assets/instructions/onboarding-flow.md +408 -0
  10. package/agent-assets/instructions/standard-folder-structure.md +125 -0
  11. package/agent-assets/reference/app-creation.md +381 -0
  12. package/agent-assets/reference/architecture.md +406 -0
  13. package/agent-assets/reference/local-dev.md +138 -0
  14. package/agent-assets/reference/mcp-integration.md +424 -0
  15. package/agent-assets/reference/workspace-config.md +415 -0
  16. package/agent-assets/rules/descix_mcp.template.mdc +243 -0
  17. package/bin/descix.js +3930 -0
  18. package/bin/mcp-server.js +382 -0
  19. package/descix-cli-1.0.0.tgz +0 -0
  20. package/lib/agent-files.js +162 -0
  21. package/lib/api-client.js +458 -0
  22. package/lib/auth-guard.js +67 -0
  23. package/lib/commands/app-wizard.js +627 -0
  24. package/lib/commands/auth.js +407 -0
  25. package/lib/commands/buy.js +207 -0
  26. package/lib/commands/clone.js +91 -0
  27. package/lib/commands/config.js +155 -0
  28. package/lib/commands/doctor.js +277 -0
  29. package/lib/commands/folder.js +291 -0
  30. package/lib/commands/init.js +257 -0
  31. package/lib/commands/kb.js +654 -0
  32. package/lib/commands/mcp.js +273 -0
  33. package/lib/commands/serve.js +12 -0
  34. package/lib/commands/status.js +239 -0
  35. package/lib/commands/update.js +653 -0
  36. package/lib/core/Chunker.js +610 -0
  37. package/lib/core/Hydrator.js +801 -0
  38. package/lib/core/PathContext.js +335 -0
  39. package/lib/core/Syncer.js +310 -0
  40. package/lib/core/index.js +32 -0
  41. package/lib/global-config.js +86 -0
  42. package/lib/google-storage-adc.js +511 -0
  43. package/lib/wallet-file.js +232 -0
  44. package/lib/wizard/setup.js +502 -0
  45. package/lib/workspace-config.js +712 -0
  46. package/lib/workspace-utils.js +126 -0
  47. package/package.json +41 -0
  48. package/templates/agent-claude.md +226 -0
  49. package/templates/agent-cline.md +133 -0
  50. package/templates/agent-copilot.md +133 -0
  51. package/templates/agent-cursor.md +133 -0
  52. package/templates/descix_mcp.mdc +122 -0
  53. package/templates/drive/README.md +42 -0
  54. package/templates/drive/app/assets/app_description.md +13 -0
  55. package/templates/drive/app/assets/icon.png +0 -0
  56. package/templates/drive/app/assets/system_instructions.md +17 -0
  57. package/templates/drive/app/kb/General/README.md +25 -0
  58. package/templates/drive/community/Apps/.gitkeep +4 -0
  59. package/templates/drive/community/community_assets/community_description.md +15 -0
  60. package/templates/drive/community/community_assets/icon.png +0 -0
  61. package/templates/drive_templates/README.md +104 -0
  62. package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
  63. package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
  64. package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
  65. package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
  66. package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
  67. package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
  68. package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
  69. package/templates/scaffolds/README.md +62 -0
  70. package/templates/scaffolds/microservice/Dockerfile +19 -0
  71. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
  72. package/templates/scaffolds/microservice/app.js +50 -0
  73. package/templates/scaffolds/microservice/app.yaml +8 -0
  74. package/templates/scaffolds/microservice/defaults-config.json +5 -0
  75. package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
  76. package/templates/scaffolds/microservice/manifest.json +49 -0
  77. package/templates/scaffolds/microservice/package-lock.json +2645 -0
  78. package/templates/scaffolds/microservice/package.json +29 -0
  79. package/templates/scaffolds/microservice/scripts/register.js +55 -0
  80. package/templates/scaffolds/microservice/services/apiFront.js +75 -0
  81. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
  82. package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
  83. package/templates/scaffolds/microservice/services/utils.js +247 -0
  84. package/templates/scaffolds/microservice/setup-schema.json +18 -0
  85. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
  86. package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
  87. package/templates/scaffolds/site/README.md +51 -0
  88. package/templates/scaffolds/site/app.js +22 -0
  89. package/templates/scaffolds/site/index.html +42 -0
  90. package/templates/scaffolds/site/styles.css +123 -0
  91. package/test-hydration.js +150 -0
  92. package/vendor/mcp/transports/http-sse-transport.js +196 -0
  93. package/vendor/mcp/transports/index.js +15 -0
  94. package/vendor/mcp/transports/stdio-transport.js +118 -0
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Authentication Commands - SDK Architecture V2
3
+ *
4
+ * login, logout, whoami, reconnect
5
+ * All operations use HTTP API client - no service imports
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ import { DeSciXApiClient } from '../api-client.js';
11
+ import { WalletFileManager } from '../wallet-file.js';
12
+ import { WorkspaceConfig } from '../workspace-config.js';
13
+ import { requireAuth } from '../auth-guard.js';
14
+ import { exec } from 'child_process';
15
+ import { promisify } from 'util';
16
+ import * as fs from 'fs/promises';
17
+
18
+ const execAsync = promisify(exec);
19
+ const POLL_INTERVAL_MS = 3000; // 3 seconds
20
+ const MAX_POLL_TIME_MS = 15 * 60 * 1000; // 15 minutes
21
+
22
+ /**
23
+ * Open browser to URL
24
+ */
25
+ async function openBrowser(url) {
26
+ const platform = process.platform;
27
+ let command;
28
+
29
+ if (platform === 'darwin') {
30
+ command = `open "${url}"`;
31
+ } else if (platform === 'win32') {
32
+ command = `start "" "${url}"`;
33
+ } else {
34
+ command = `xdg-open "${url}"`;
35
+ }
36
+
37
+ try {
38
+ await execAsync(command);
39
+ return true;
40
+ } catch (error) {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Request device login via HTTP
47
+ * @param {DeSciXApiClient} apiClient - The API client
48
+ */
49
+ async function requestDeviceLogin(apiClient) {
50
+ const response = await apiClient.invoke('device_request_login', {}, { allowGuest: true });
51
+ return response.message;
52
+ }
53
+
54
+ /**
55
+ * Poll for device login completion via HTTP
56
+ */
57
+ async function pollDeviceLogin(apiClient, deviceCode, onStatus = null) {
58
+ const startTime = Date.now();
59
+
60
+ while (true) {
61
+ if (Date.now() - startTime > MAX_POLL_TIME_MS) {
62
+ throw new Error('Device login request expired. Please try again.');
63
+ }
64
+
65
+ try {
66
+ const response = await apiClient.invoke('device_check_status', { device_code: deviceCode }, { allowGuest: true });
67
+ const status = response.message.status;
68
+
69
+ if (status === 'complete') {
70
+ return {
71
+ user_id: response.message.user_id,
72
+ session_token: response.message.session_token,
73
+ wallet_address: response.message.wallet_address,
74
+ signature: response.message.signature,
75
+ community_id: response.message.community_id || 'descix',
76
+ token_symbol: response.message.token_symbol || 'DAITA'
77
+ };
78
+ }
79
+
80
+ if (status === 'expired') {
81
+ throw new Error('Device login request expired. Please try again.');
82
+ }
83
+
84
+ if (onStatus) {
85
+ onStatus(status);
86
+ }
87
+
88
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
89
+ } catch (error) {
90
+ if (error.message.includes('expired') || error.message.includes('timeout')) {
91
+ throw error;
92
+ }
93
+ // Continue polling on other errors
94
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Device login command
101
+ */
102
+ export async function loginDevice(options = {}) {
103
+ const apiClient = new DeSciXApiClient({ baseUrl: options.url });
104
+
105
+ console.log(chalk.cyan('\n🔐 DeSciX Device Login\n'));
106
+
107
+ const spinner = ora('Requesting device login...').start();
108
+
109
+ // Ensure we have the base URL resolved
110
+ await apiClient.ensureBaseUrl();
111
+ console.log(chalk.gray(` API: ${apiClient.baseUrl}\n`));
112
+
113
+ try {
114
+ // Detect workspace root
115
+ let workspaceRoot;
116
+ let foundRoot;
117
+
118
+ if (options.workspaceRoot) {
119
+ workspaceRoot = options.workspaceRoot;
120
+ foundRoot = workspaceRoot;
121
+ } else {
122
+ const startDir = process.cwd();
123
+ foundRoot = await WorkspaceConfig.findWorkspaceRoot(startDir);
124
+ workspaceRoot = foundRoot || startDir;
125
+ }
126
+
127
+ // Request device login
128
+ const request = await requestDeviceLogin(apiClient);
129
+
130
+ spinner.succeed('Device login request created');
131
+
132
+ console.log(chalk.yellow('\n📱 Browser will open automatically...\n'));
133
+ console.log(chalk.white('In the browser, you will need to:'));
134
+ console.log(chalk.gray(' 1. Enter your email address'));
135
+ console.log(chalk.gray(' 2. Verify your email with the code sent'));
136
+ console.log(chalk.gray(' 3. Accept the Terms of Service'));
137
+ console.log(chalk.gray(' 4. Connect your crypto wallet'));
138
+ console.log(chalk.gray(' 5. Sign the message to prove wallet ownership'));
139
+ console.log('');
140
+
141
+ spinner.start('Opening browser...');
142
+
143
+ // Open browser
144
+ const apiUrl = apiClient.baseUrl;
145
+ const verificationUrl = request.verification_url || `${apiUrl}/wallet?code=${request.user_code}`;
146
+ const opened = await openBrowser(verificationUrl);
147
+ if (opened) {
148
+ spinner.succeed('Browser opened');
149
+ console.log(chalk.yellow(`\nURL: ${verificationUrl}\n`));
150
+ } else {
151
+ spinner.warn('Could not open browser automatically');
152
+ console.log(chalk.yellow(`\nPlease open: ${verificationUrl}\n`));
153
+ }
154
+
155
+ // Poll for completion
156
+ spinner.start('Waiting for authentication...');
157
+ const credentials = await pollDeviceLogin(apiClient, request.device_code, (status) => {
158
+ spinner.text = `Waiting for authentication... (${status})`;
159
+ });
160
+
161
+ // Save credentials
162
+ spinner.start('Saving credentials...');
163
+ const walletData = {
164
+ walletAddress: credentials.wallet_address,
165
+ signature: credentials.signature,
166
+ tokenSymbol: credentials.token_symbol,
167
+ communityId: credentials.community_id,
168
+ userId: credentials.user_id,
169
+ email: credentials.email,
170
+ sessionToken: credentials.session_token,
171
+ expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString()
172
+ };
173
+
174
+ const walletPath = WalletFileManager.getProjectWalletPath(workspaceRoot);
175
+ await WalletFileManager.saveWalletFile(walletPath, walletData);
176
+
177
+ spinner.succeed(chalk.green('Login successful!'));
178
+ console.log(chalk.cyan(`\n✅ Credentials saved to:`));
179
+ console.log(chalk.gray(` ${walletPath}\n`));
180
+
181
+ // Sync auto-purchase apps
182
+ spinner.start('Syncing app entitlements...');
183
+ try {
184
+ apiClient.setCredentials({
185
+ userId: credentials.user_id,
186
+ accessToken: credentials.session_token,
187
+ walletAddress: credentials.wallet_address,
188
+ signature: credentials.signature
189
+ });
190
+
191
+ const syncResponse = await apiClient.invoke('sync_auto_purchases', {}, { allowGuest: false });
192
+ const syncResult = syncResponse.message || syncResponse;
193
+
194
+ if (syncResult.apps_purchased && syncResult.apps_purchased.length > 0) {
195
+ spinner.succeed(chalk.green(`Synced ${syncResult.apps_purchased.length} app(s)`));
196
+ } else {
197
+ spinner.succeed(chalk.gray('App entitlements up to date'));
198
+ }
199
+ } catch (error) {
200
+ spinner.warn(chalk.yellow('Could not sync app entitlements'));
201
+ console.log(chalk.gray(` ${error.message}\n`));
202
+ }
203
+
204
+ console.log(chalk.white('You can now use all DeSciX CLI commands!\n'));
205
+
206
+ } catch (error) {
207
+ spinner.fail(chalk.red('Login failed'));
208
+ console.error(chalk.red(error.message));
209
+
210
+ if (error.message.includes('expired')) {
211
+ console.log(chalk.yellow('💡 Tip: Device codes expire after 15 minutes.'));
212
+ console.log(chalk.yellow(' Run the login command again to get a new code.\n'));
213
+ } else {
214
+ console.log(chalk.yellow('💡 Tip: Make sure you:'));
215
+ console.log(chalk.yellow(' - Have an internet connection'));
216
+ console.log(chalk.yellow(' - Can access the DeSciX platform'));
217
+ console.log(chalk.yellow(' - Complete all steps in the browser'));
218
+ console.log(chalk.yellow(` - API URL is correct: ${apiClient.baseUrl || 'unknown'}`));
219
+ if (apiClient.baseUrl?.includes('localhost')) {
220
+ console.log(chalk.yellow(' - Local backend server is running'));
221
+ }
222
+ console.log('');
223
+ }
224
+ throw error;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Direct wallet login command (not yet implemented)
230
+ */
231
+ export async function loginWallet() {
232
+ console.log(chalk.cyan('\n🔐 DeSciX Wallet Login\n'));
233
+ console.log(chalk.yellow('\n⚠️ Direct wallet login requires a Web3 provider.\n'));
234
+ console.log(chalk.white('For CLI/MCP, device login is recommended:\n'));
235
+ console.log(chalk.cyan(' descix login\n'));
236
+ console.log(chalk.gray('This opens your browser where you can connect your wallet.\n'));
237
+ throw new Error('Direct wallet login not yet implemented for CLI. Use device login instead.');
238
+ }
239
+
240
+ /**
241
+ * Reconnect using saved wallet credentials via HTTP
242
+ */
243
+ export async function reconnect() {
244
+ const apiClient = new DeSciXApiClient();
245
+
246
+ const spinner = ora('Loading wallet credentials...').start();
247
+
248
+ try {
249
+ const walletPath = await WalletFileManager.findWalletFile(process.cwd());
250
+ if (!walletPath) {
251
+ throw new Error('No wallet file found. Run "descix login" first.');
252
+ }
253
+
254
+ const walletInfo = await WalletFileManager.loadWalletFile(walletPath);
255
+ if (!walletInfo) {
256
+ throw new Error('Failed to load wallet file. Run "descix login" to re-authenticate.');
257
+ }
258
+
259
+ spinner.text = 'Reconnecting with wallet...';
260
+
261
+ const result = await apiClient.invoke('reconnect_by_wallet', {
262
+ wallet_address: walletInfo.walletAddress,
263
+ signature: walletInfo.signature
264
+ }, {
265
+ credentials: {
266
+ walletAddress: walletInfo.walletAddress,
267
+ signature: walletInfo.signature
268
+ },
269
+ allowGuest: true
270
+ });
271
+
272
+ if (result.status !== 'OK') {
273
+ throw new Error(result.message || 'Reconnection failed');
274
+ }
275
+
276
+ const sessionInfo = result.message?.sessionInfo || result.message;
277
+
278
+ const updatedWalletData = {
279
+ ...walletInfo,
280
+ sessionToken: sessionInfo?.access_token || sessionInfo?.session_token || walletInfo.sessionToken,
281
+ userId: sessionInfo?.id || sessionInfo?.user_id || walletInfo.userId,
282
+ expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString()
283
+ };
284
+ await WalletFileManager.saveWalletFile(walletPath, updatedWalletData);
285
+
286
+ spinner.succeed(chalk.green('Reconnected successfully!'));
287
+ console.log(chalk.cyan(`\n✅ Status: ${result.auth_status || 'CONNECTED'}`));
288
+ console.log(chalk.gray(` User: ${sessionInfo?.email || sessionInfo?.id || walletInfo.userId}`));
289
+ console.log(chalk.gray(` Wallet: ${walletInfo.walletAddress?.substring(0, 10)}...`));
290
+ console.log(chalk.gray(` Session refreshed\n`));
291
+
292
+ } catch (error) {
293
+ spinner.fail(chalk.red('Reconnection failed'));
294
+ console.error(chalk.red(error.message));
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Show current authentication status via HTTP
301
+ */
302
+ export async function whoami() {
303
+ const apiClient = new DeSciXApiClient();
304
+
305
+ const spinner = ora('Checking authentication status...').start();
306
+
307
+ try {
308
+ await requireAuth(apiClient);
309
+
310
+ const response = await apiClient.invoke('check_staked_status', {}, { allowGuest: false });
311
+ const userInfo = response.message;
312
+
313
+ const walletPath = await WalletFileManager.findWalletFile(process.cwd());
314
+ const walletInfo = walletPath ? await WalletFileManager.loadWalletFile(walletPath) : null;
315
+ const isValid = walletInfo ? WalletFileManager.hasValidSession(walletInfo) : false;
316
+
317
+ spinner.succeed(chalk.green('Authentication status'));
318
+
319
+ console.log(chalk.cyan('\n📋 Current Session:\n'));
320
+ console.log(chalk.white(` User ID: ${userInfo?.id || userInfo?.user_id || walletInfo?.userId || 'Unknown'}`));
321
+ console.log(chalk.white(` Email: ${userInfo?.email || walletInfo?.email || 'N/A'}`));
322
+ if (walletInfo) {
323
+ console.log(chalk.white(` Wallet: ${walletInfo.walletAddress?.substring(0, 10)}...${walletInfo.walletAddress?.slice(-6) || ''}`));
324
+ if (walletInfo.communityId) {
325
+ console.log(chalk.white(` Community: ${walletInfo.communityId}`));
326
+ }
327
+ if (walletInfo.tokenSymbol) {
328
+ console.log(chalk.white(` Token: ${walletInfo.tokenSymbol}`));
329
+ }
330
+ console.log(chalk.white(` Session: ${isValid ? chalk.green('Valid') : chalk.red('Expired')}`));
331
+
332
+ if (walletInfo.expiresAt) {
333
+ const expiresDate = new Date(walletInfo.expiresAt);
334
+ const now = new Date();
335
+ if (expiresDate > now) {
336
+ const hoursLeft = Math.round((expiresDate - now) / (1000 * 60 * 60));
337
+ console.log(chalk.white(` Expires: ${hoursLeft}h remaining`));
338
+ } else {
339
+ console.log(chalk.white(` Expires: ${chalk.red('Expired')}`));
340
+ }
341
+ }
342
+ }
343
+
344
+ if (userInfo) {
345
+ console.log(chalk.cyan('\n📊 Account Details:\n'));
346
+ console.log(chalk.white(` Balance: ${userInfo.custodial_usd_balance || userInfo.balance || 0} USDCX`));
347
+ console.log(chalk.white(` Staked: ${userInfo.is_staked ? chalk.green('Yes') : chalk.yellow('No')}`));
348
+
349
+ try {
350
+ const purchasesResponse = await apiClient.invoke('fetch_my_purchases', { product_type: 'APP' }, { allowGuest: false });
351
+ const purchases = purchasesResponse.message || {};
352
+ const communities = purchases.communities || [];
353
+ const apps = purchases.apps || purchases.products || [];
354
+
355
+ console.log(chalk.white(` Communities: ${communities.length}`));
356
+ console.log(chalk.white(` Apps: ${apps.length}`));
357
+ if (apps.length > 0) {
358
+ apps.slice(0, 5).forEach(a => {
359
+ const id = a.app_id || a.appId || a.id || a.product_id;
360
+ if (id) console.log(chalk.gray(` - ${id}`));
361
+ });
362
+ if (apps.length > 5) console.log(chalk.gray(` ... and ${apps.length - 5} more`));
363
+ }
364
+ } catch (err) {
365
+ // Ignore purchase fetch errors
366
+ }
367
+ }
368
+
369
+ console.log('');
370
+
371
+ if (walletInfo && !isValid) {
372
+ console.log(chalk.yellow(' ⚠️ Session expired. Run "descix reconnect" to refresh.\n'));
373
+ }
374
+
375
+ } catch (error) {
376
+ spinner.fail(chalk.red('Failed to check status'));
377
+ console.error(chalk.red(error.message));
378
+ throw error;
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Logout - clear credentials
384
+ */
385
+ export async function logout() {
386
+ const spinner = ora('Logging out...').start();
387
+
388
+ try {
389
+ const walletPath = await WalletFileManager.findWalletFile(process.cwd());
390
+
391
+ if (!walletPath) {
392
+ spinner.info(chalk.yellow('Not logged in'));
393
+ console.log(chalk.gray('\n No credentials to remove.\n'));
394
+ return;
395
+ }
396
+
397
+ await fs.unlink(walletPath);
398
+
399
+ spinner.succeed(chalk.green('Logged out'));
400
+ console.log(chalk.cyan('\n✅ Credentials removed\n'));
401
+
402
+ } catch (error) {
403
+ spinner.fail(chalk.red('Logout failed'));
404
+ console.error(chalk.red(error.message));
405
+ throw error;
406
+ }
407
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Buy Commands
3
+ *
4
+ * Commands for creating and managing crypto payment quotes.
5
+ * Used for purchasing USDCX or Community tokens with cryptocurrency.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { DeSciXApiClient } from '../api-client.js';
10
+
11
+ /**
12
+ * Create a crypto payment quote
13
+ * @param {Object} options - Quote options
14
+ */
15
+ export async function createQuote(options) {
16
+ const apiClient = new DeSciXApiClient();
17
+
18
+ const params = {
19
+ usd_amount: parseFloat(options.amount),
20
+ chain: options.chain.toLowerCase(),
21
+ purchase_type: options.type || 'usdcx'
22
+ };
23
+
24
+ // Add optional email for guest mode
25
+ if (options.email) {
26
+ params.email = options.email;
27
+ }
28
+
29
+ // Add community_id if purchasing community tokens
30
+ if (options.community) {
31
+ params.community_id = options.community;
32
+ params.purchase_type = 'community_token';
33
+ }
34
+
35
+ console.log(chalk.cyan(`Creating ${params.purchase_type} quote for $${params.usd_amount} via ${params.chain}...`));
36
+
37
+ try {
38
+ const response = await apiClient.invoke('create_crypto_quote', params, { allowGuest: true });
39
+
40
+ // API returns { status, auth_status, message } where message contains the quote data
41
+ const result = response?.message || response;
42
+
43
+ if (!result?.quote_id) {
44
+ throw new Error('Failed to create quote - no quote_id returned');
45
+ }
46
+
47
+ console.log(chalk.green('\n✓ Quote created successfully!\n'));
48
+ console.log(chalk.bold('Quote Details:'));
49
+ console.log(` Quote ID: ${chalk.yellow(result.quote_id)}`);
50
+ console.log(` Chain: ${result.chain_name || result.chain}`);
51
+ console.log(` Amount: ${result.amount_formatted || `${result.amount} ${result.asset}`}`);
52
+ console.log(` USD Value: ${result.usd_amount_formatted || `$${result.usd_amount}`}`);
53
+ console.log(` Deposit Addr: ${chalk.cyan(result.deposit_address)}`);
54
+ console.log(` Expires: ${new Date(result.expires_at).toLocaleString()}`);
55
+
56
+ console.log(chalk.bold('\nInstructions:'));
57
+ console.log(` 1. Send exactly ${chalk.yellow(result.amount_formatted || `${result.amount} ${result.asset}`)} to the deposit address`);
58
+ console.log(` 2. Wait for confirmation (the system will detect your payment)`);
59
+ console.log(` 3. Check status with: ${chalk.cyan(`descix buy status ${result.quote_id}`)}`);
60
+
61
+ return result;
62
+ } catch (error) {
63
+ console.error(chalk.red(`Error creating quote: ${error.message}`));
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Check the status of a quote
70
+ * @param {string} quoteId - Quote ID to check
71
+ */
72
+ export async function checkStatus(quoteId) {
73
+ const apiClient = new DeSciXApiClient();
74
+
75
+ console.log(chalk.cyan(`Checking status of quote ${quoteId}...`));
76
+
77
+ try {
78
+ const response = await apiClient.invoke('get_quote_status', { quote_id: quoteId }, { allowGuest: true });
79
+
80
+ // API returns { status, auth_status, message } where message contains the quote data
81
+ const result = response?.message || response;
82
+
83
+ if (!result) {
84
+ console.log(chalk.red('Quote not found'));
85
+ return null;
86
+ }
87
+
88
+ const statusColor = {
89
+ 'pending': chalk.yellow,
90
+ 'confirmed': chalk.green,
91
+ 'fulfilled': chalk.green.bold,
92
+ 'expired': chalk.red,
93
+ 'amount_mismatch': chalk.red,
94
+ 'failed': chalk.red
95
+ }[result.status] || chalk.white;
96
+
97
+ console.log(chalk.bold('\nQuote Status:'));
98
+ console.log(` Quote ID: ${result.quote_id}`);
99
+ console.log(` Status: ${statusColor(result.status.toUpperCase())}`);
100
+ console.log(` Chain: ${result.chain_name || result.chain}`);
101
+ console.log(` Amount: ${result.amount_formatted || `${result.crypto_amount || result.amount} ${result.asset || result.chain?.toUpperCase()}`}`);
102
+ console.log(` USD Value: ${result.usd_amount_formatted || `$${result.usd_amount}`}`);
103
+ console.log(` Deposit Addr: ${result.deposit_address || result.crypto_address}`);
104
+
105
+ if (result.status === 'pending') {
106
+ const expiresAt = new Date(result.expires_at);
107
+ const now = new Date();
108
+ const minutesLeft = Math.round((expiresAt - now) / 60000);
109
+
110
+ if (minutesLeft > 0) {
111
+ console.log(` Expires in: ${minutesLeft} minutes`);
112
+ } else {
113
+ console.log(` Expires in: ${chalk.red('EXPIRED')}`);
114
+ }
115
+
116
+ console.log(chalk.bold('\nWaiting for payment...'));
117
+ console.log(` Send ${chalk.yellow(result.amount_formatted || `${result.crypto_amount || result.amount} ${result.asset || result.chain?.toUpperCase()}`)} to:`);
118
+ console.log(` ${chalk.cyan(result.deposit_address || result.crypto_address)}`);
119
+ } else if (result.status === 'fulfilled') {
120
+ console.log(chalk.green('\n✓ Payment completed and credited!'));
121
+ if (result.payment_tx_hash) {
122
+ console.log(` Transaction: ${result.payment_tx_hash}`);
123
+ }
124
+ } else if (result.status === 'confirmed') {
125
+ console.log(chalk.yellow('\n⏳ Payment detected, awaiting confirmation...'));
126
+ if (result.confirmations_received) {
127
+ console.log(` Confirmations: ${result.confirmations_received}`);
128
+ }
129
+ } else if (result.status === 'amount_mismatch') {
130
+ console.log(chalk.red('\n⚠ Amount mismatch detected'));
131
+ if (result.actual_amount) {
132
+ console.log(` Expected: ${result.crypto_amount}, Received: ${result.actual_amount}`);
133
+ }
134
+ }
135
+
136
+ return result;
137
+ } catch (error) {
138
+ console.error(chalk.red(`Error checking status: ${error.message}`));
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Poll for quote status with exponential backoff
145
+ * @param {string} quoteId - Quote ID to poll
146
+ * @param {Object} options - Polling options
147
+ */
148
+ export async function pollStatus(quoteId, options = {}) {
149
+ const maxAttempts = options.maxAttempts || 120;
150
+ const initialInterval = options.initialInterval || 5000;
151
+ const maxInterval = options.maxInterval || 60000;
152
+
153
+ let attempt = 0;
154
+ let interval = initialInterval;
155
+
156
+ console.log(chalk.cyan(`Polling for quote ${quoteId} status (Ctrl+C to stop)...`));
157
+
158
+ while (attempt < maxAttempts) {
159
+ try {
160
+ const result = await checkStatus(quoteId);
161
+
162
+ if (!result) {
163
+ break;
164
+ }
165
+
166
+ if (['fulfilled', 'expired', 'failed', 'amount_mismatch'].includes(result.status)) {
167
+ return result;
168
+ }
169
+
170
+ // Exponential backoff
171
+ await new Promise(resolve => setTimeout(resolve, interval));
172
+ interval = Math.min(interval * 1.5, maxInterval);
173
+ attempt++;
174
+
175
+ console.log(chalk.dim(`\n--- Checking again (attempt ${attempt}/${maxAttempts}) ---`));
176
+ } catch (error) {
177
+ console.error(chalk.red(`Poll error: ${error.message}`));
178
+ await new Promise(resolve => setTimeout(resolve, interval));
179
+ attempt++;
180
+ }
181
+ }
182
+
183
+ console.log(chalk.yellow('\nMax poll attempts reached. Check status manually.'));
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * List supported chains
189
+ */
190
+ export function listChains() {
191
+ console.log(chalk.bold('Supported chains for crypto payments:\n'));
192
+
193
+ const chains = [
194
+ { name: 'polygon', symbol: 'MATIC', description: 'Polygon (recommended - low fees)' },
195
+ { name: 'ethereum', symbol: 'ETH', description: 'Ethereum mainnet' },
196
+ { name: 'bnb', symbol: 'BNB', description: 'BNB Smart Chain' },
197
+ { name: 'bitcoin', symbol: 'BTC', description: 'Bitcoin (coming soon)' },
198
+ { name: 'dogecoin', symbol: 'DOGE', description: 'Dogecoin (coming soon)' }
199
+ ];
200
+
201
+ for (const chain of chains) {
202
+ console.log(` ${chalk.cyan(chain.name.padEnd(12))} ${chain.symbol.padEnd(6)} ${chain.description}`);
203
+ }
204
+
205
+ console.log(chalk.dim('\nNote: Bitcoin and Dogecoin support is coming soon.'));
206
+ }
207
+