@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.
- package/README.md +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- 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
|
+
|