@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,458 @@
1
+ /**
2
+ * DeSciX HTTP-Only API Client
3
+ *
4
+ * Self-contained HTTP client for CLI/MCP.
5
+ * No service imports - all communication via HTTP.
6
+ * Uses PathContext for workspace/config resolution.
7
+ */
8
+
9
+ import path from 'path';
10
+ import axios from 'axios';
11
+ import { PathContext } from './core/PathContext.js';
12
+
13
+ export class DeSciXApiClient {
14
+ constructor(options = {}) {
15
+ this.baseUrl = options.baseUrl || null; // Will be loaded async
16
+ this.credentials = null;
17
+ this.workspaceRoot = options.workspaceRoot || null; // Set by initialize()
18
+ this._pathContext = null; // Cached PathContext
19
+ this._initialized = false;
20
+ }
21
+
22
+ /**
23
+ * Initialize the client by finding workspace root and loading config
24
+ * This is the primary initialization method - call before any API operations
25
+ *
26
+ * Uses PathContext for unified path resolution.
27
+ *
28
+ * @returns {Promise<void>}
29
+ */
30
+ async initialize() {
31
+ if (this._initialized) {
32
+ return;
33
+ }
34
+
35
+ // Step 1: Try to load PathContext (will find IDE marker and load workspace.json)
36
+ this._pathContext = await PathContext.tryLoad();
37
+
38
+ if (this._pathContext) {
39
+ this.workspaceRoot = this._pathContext.getWorkspaceRoot();
40
+ } else {
41
+ this.workspaceRoot = null;
42
+ }
43
+
44
+ // Step 2: Set API URL from PathContext config or environment (only if not already set)
45
+ if (!this.baseUrl) {
46
+ this.baseUrl = await this.detectApiUrl();
47
+ }
48
+
49
+ // Step 3: Load credentials from known wallet location
50
+ await this.loadCredentials();
51
+
52
+ this._initialized = true;
53
+ }
54
+
55
+ /**
56
+ * Detect API URL from workspace config or environment
57
+ * @returns {Promise<string>} API base URL
58
+ */
59
+ async detectApiUrl() {
60
+ const PRODUCTION_URL = 'https://descix.net';
61
+
62
+ // Check environment variable first (highest priority)
63
+ if (process.env.DESCIX_API_URL) {
64
+ return process.env.DESCIX_API_URL;
65
+ }
66
+
67
+ // Use PathContext config if loaded (but only if it has an explicit URL, not the production default)
68
+ if (this._pathContext) {
69
+ const apiUrl = this._pathContext.getApiUrl();
70
+ if (apiUrl && apiUrl !== PRODUCTION_URL) {
71
+ return apiUrl;
72
+ }
73
+ }
74
+
75
+ // Check global config (set by `descix login --dev` or `descix config set-url`)
76
+ try {
77
+ const { GlobalConfig } = await import('./global-config.js');
78
+ const gc = await GlobalConfig.load();
79
+ if (gc.api_url && gc.api_url !== PRODUCTION_URL) return gc.api_url;
80
+ } catch {}
81
+
82
+ // Default to production
83
+ return PRODUCTION_URL;
84
+ }
85
+
86
+ /**
87
+ * Ensure client is initialized (workspace root found, config loaded)
88
+ * @returns {Promise<void>}
89
+ */
90
+ async ensureInitialized() {
91
+ if (!this._initialized) {
92
+ await this.initialize();
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Ensure baseUrl is loaded (calls initialize if needed)
98
+ * @returns {Promise<string>} Base URL
99
+ */
100
+ async ensureBaseUrl() {
101
+ await this.ensureInitialized();
102
+ return this.baseUrl;
103
+ }
104
+
105
+ /**
106
+ * Build request body for API call
107
+ * @param {string} command - Command name
108
+ * @param {Object} params - Command parameters
109
+ * @param {Object} credentials - Optional credentials override
110
+ * @returns {Object} Request body
111
+ */
112
+ buildRequestBody(command, params = {}, credentials = null) {
113
+ const creds = credentials || this.credentials || {};
114
+
115
+ const requestBody = {
116
+ command,
117
+ params,
118
+ user_id: creds.userId || null,
119
+ access_token: creds.accessToken || null,
120
+ wallet_address: creds.walletAddress || null,
121
+ signature: creds.signature || null,
122
+ guild_id: params.guild_id || null
123
+ };
124
+
125
+ if (params.streaming) {
126
+ requestBody.streaming = true;
127
+ }
128
+
129
+ return requestBody;
130
+ }
131
+
132
+ /**
133
+ * Ensure we have a valid session by calling reconnect_by_wallet if needed
134
+ * This is called automatically before authenticated API calls
135
+ * @returns {Promise<void>}
136
+ */
137
+ async ensureSession() {
138
+ // If we already have an access token, assume it's valid (will retry if expired)
139
+ if (this.credentials?.accessToken) {
140
+ return;
141
+ }
142
+
143
+ // If we have wallet credentials but no session, get one via reconnect_by_wallet
144
+ if (this.credentials?.walletAddress && this.credentials?.signature) {
145
+ try {
146
+ // Use invokeRaw to avoid session check recursion
147
+ // We pass skipSessionCheck: true to invokeRaw to be extra safe
148
+ const result = await this.invokeRaw('reconnect_by_wallet', {
149
+ wallet_address: this.credentials.walletAddress,
150
+ signature: this.credentials.signature
151
+ }, null, { skipSessionCheck: true });
152
+
153
+ if (result.status === 'OK' && result.message?.sessionInfo?.access_token) {
154
+ this.credentials.accessToken = result.message.sessionInfo.access_token;
155
+ // Also update userId if returned (in case it was missing)
156
+ if (result.message.sessionInfo.id) {
157
+ this.credentials.userId = result.message.sessionInfo.id;
158
+ }
159
+
160
+ // Save updated credentials to wallet file
161
+ try {
162
+ const { WalletFileManager } = await import('./wallet-file.js');
163
+ const walletPath = await WalletFileManager.findWalletFile(this.workspaceRoot || process.cwd());
164
+ if (walletPath) {
165
+ const walletData = await WalletFileManager.loadWalletFile(walletPath);
166
+ if (walletData) {
167
+ walletData.sessionToken = this.credentials.accessToken;
168
+ walletData.userId = this.credentials.userId;
169
+ walletData.expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
170
+ await WalletFileManager.saveWalletFile(walletPath, walletData);
171
+ }
172
+ }
173
+ } catch (saveErr) {
174
+ console.error('[ApiClient] Failed to save refreshed session to wallet file:', saveErr.message);
175
+ }
176
+ }
177
+ } catch (error) {
178
+ // If reconnect fails, continue anyway - the actual request will fail with proper error
179
+ console.error('[ApiClient] Failed to create session via wallet:', error.message);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Invoke a command via HTTP (raw, no session management)
186
+ * Used internally for auth commands to avoid recursion
187
+ * @param {string} command - Command name
188
+ * @param {Object} params - Command parameters
189
+ * @param {Object} credentials - Optional credentials override
190
+ * @returns {Promise<Object>} Response data
191
+ */
192
+ async invokeRaw(command, params = {}, credentials = null, options = {}) {
193
+ console.log(`[ApiClient] invoking ${command}...`);
194
+ await this.ensureBaseUrl();
195
+ console.log(`[ApiClient] Base URL: ${this.baseUrl}`);
196
+
197
+ const requestBody = this.buildRequestBody(command, params, credentials);
198
+ if (options.skipSessionCheck) {
199
+ requestBody.access_token = null; // Force guest-like call for refresh
200
+ }
201
+ const url = `${this.baseUrl}/apifront/`;
202
+ console.log(`[ApiClient] POST ${url}`);
203
+
204
+ const axiosConfig = {
205
+ headers: { 'Content-Type': 'application/json' },
206
+ timeout: 300000
207
+ };
208
+
209
+ if (this.baseUrl.includes('localhost') || this.baseUrl.includes('127.0.0.1')) {
210
+ const https = await import('https');
211
+ axiosConfig.httpsAgent = new https.Agent({
212
+ rejectUnauthorized: false
213
+ });
214
+ }
215
+
216
+ try {
217
+ const response = await axios.post(url, requestBody, axiosConfig);
218
+ console.log(`[ApiClient] Response status: ${response.status}`);
219
+ const data = response.data;
220
+
221
+ if (data.status !== 'OK') {
222
+ const error = new Error(data.message || 'API request failed');
223
+ error.status = data.status;
224
+ error.response = data;
225
+ throw error;
226
+ }
227
+
228
+ return data;
229
+ } catch (error) {
230
+ console.error(`[ApiClient] Error invoking ${command}:`, error.message);
231
+ throw error;
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Invoke a command via HTTP with automatic session management
237
+ * @param {string} command - Command name
238
+ * @param {Object} params - Command parameters
239
+ * @param {Object} options - Optional { credentials, allowGuest, skipSessionCheck }
240
+ * @returns {Promise<Object>} Response data
241
+ */
242
+ async invoke(command, params = {}, options = {}) {
243
+ // Ensure baseUrl is loaded
244
+ await this.ensureBaseUrl();
245
+
246
+ // Load credentials if not provided
247
+ let credentials = options.credentials || this.credentials;
248
+ if (!credentials && !options.allowGuest) {
249
+ credentials = await this.loadCredentials();
250
+ }
251
+
252
+ // Ensure we have a session (unless this is a guest command or session check is skipped)
253
+ if (!options.allowGuest && !options.skipSessionCheck) {
254
+ await this.ensureSession();
255
+ }
256
+
257
+ // Build request with potentially updated credentials (after ensureSession)
258
+ const requestBody = this.buildRequestBody(command, params, this.credentials || credentials);
259
+
260
+ // If we are triggering device login, we MUST NOT pass an expired session token
261
+ // or the backend will reject it before we can even request a new code.
262
+ if (command === 'device_request_login' || command === 'device_check_status') {
263
+ requestBody.access_token = null;
264
+ }
265
+
266
+ const url = `${this.baseUrl}/apifront/`;
267
+
268
+ try {
269
+ // For local development with self-signed certs, disable SSL verification
270
+ const axiosConfig = {
271
+ headers: { 'Content-Type': 'application/json' },
272
+ timeout: 300000 // 5 minute timeout for file uploads with vectorization
273
+ };
274
+
275
+ // Disable SSL verification for localhost (development only)
276
+ if (this.baseUrl.includes('localhost') || this.baseUrl.includes('127.0.0.1')) {
277
+ const https = await import('https');
278
+ axiosConfig.httpsAgent = new https.Agent({
279
+ rejectUnauthorized: false
280
+ });
281
+ }
282
+
283
+ const response = await axios.post(url, requestBody, axiosConfig);
284
+
285
+ const data = response.data;
286
+
287
+ if (data.status !== 'OK') {
288
+ const error = new Error(data.message || 'API request failed');
289
+ error.status = data.status;
290
+ error.response = data;
291
+ throw error;
292
+ }
293
+
294
+ return data;
295
+ } catch (error) {
296
+ // Check if this is an auth failure that we can retry
297
+ const isAuthError = error.response?.status === 401 ||
298
+ error.response?.auth_status === 'AUTH_FAILED' ||
299
+ error.message?.includes('session') ||
300
+ error.message?.includes('expired');
301
+
302
+ // If auth failed and we have wallet credentials, try to refresh session once
303
+ if (isAuthError &&
304
+ this.credentials?.walletAddress &&
305
+ this.credentials?.signature &&
306
+ !options._retried) {
307
+
308
+ // Clear existing access token
309
+ if (this.credentials) {
310
+ this.credentials.accessToken = null;
311
+ }
312
+
313
+ // Try to get new session
314
+ await this.ensureSession();
315
+
316
+ // If we got a new token, retry the original request
317
+ if (this.credentials?.accessToken) {
318
+ return this.invoke(command, params, { ...options, _retried: true });
319
+ } else if (command !== 'device_request_login' && command !== 'device_check_status') {
320
+ // If refresh failed (e.g. signature invalid or user not found),
321
+ // we should trigger device login if this is a CLI environment
322
+ console.log('[ApiClient] Session refresh failed, triggering device login...');
323
+ const { loginDevice } = await import('./commands/auth.js');
324
+ await loginDevice({ url: this.baseUrl, workspaceRoot: this.workspaceRoot });
325
+
326
+ // After login, credentials should be updated in the wallet file.
327
+ // Reload them and retry the request one last time.
328
+ this.credentials = null;
329
+ await this.loadCredentials();
330
+ if (this.credentials?.accessToken) {
331
+ return this.invoke(command, params, { ...options, _retried: true });
332
+ }
333
+ }
334
+ }
335
+
336
+ if (error.response) {
337
+ // Axios error with response
338
+ const errorData = error.response.data || { message: 'HTTP request failed' };
339
+ const apiError = new Error(errorData.message || `HTTP ${error.response.status}`);
340
+ apiError.status = error.response.status;
341
+ apiError.response = errorData;
342
+ throw apiError;
343
+ } else if (error.code === 'ECONNREFUSED') {
344
+ throw new Error(`Cannot connect to ${this.baseUrl}. Is the backend server running?`);
345
+ } else if (error.code === 'ETIMEDOUT') {
346
+ throw new Error(`Request to ${this.baseUrl} timed out. Please try again.`);
347
+ } else {
348
+ // Network or other error
349
+ throw error;
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Load credentials from wallet file at workspace root
356
+ * Wallet is ALWAYS at {workspaceRoot}/.descix/wallet.json
357
+ * @returns {Promise<Object|null>} Credentials object or null
358
+ */
359
+ async loadCredentials() {
360
+ if (this.credentials) {
361
+ return this.credentials;
362
+ }
363
+
364
+ // Use PathContext's workspace root if available
365
+ if (!this.workspaceRoot && this._pathContext) {
366
+ this.workspaceRoot = this._pathContext.getWorkspaceRoot();
367
+ }
368
+
369
+ // If still no workspace root, try: PathContext (IDE markers) → workspace.json → wallet.json
370
+ if (!this.workspaceRoot) {
371
+ const ctx = await PathContext.tryLoad();
372
+ if (ctx) {
373
+ this.workspaceRoot = ctx.getWorkspaceRoot();
374
+ this._pathContext = ctx;
375
+ } else {
376
+ const { WorkspaceConfig } = await import('./workspace-config.js');
377
+ this.workspaceRoot = await WorkspaceConfig.findWorkspaceRoot();
378
+ }
379
+ // Final fallback: walk up from cwd looking for .descix/wallet.json
380
+ if (!this.workspaceRoot) {
381
+ const fs = await import('fs/promises');
382
+ let dir = path.resolve(process.cwd());
383
+ const root = path.parse(dir).root;
384
+ while (dir !== root) {
385
+ try {
386
+ await fs.access(path.join(dir, '.descix', 'wallet.json'));
387
+ this.workspaceRoot = dir;
388
+ break;
389
+ } catch { /* not here, keep walking */ }
390
+ const parent = path.dirname(dir);
391
+ if (parent === dir) break;
392
+ dir = parent;
393
+ }
394
+ if (!this.workspaceRoot) return null;
395
+ }
396
+ }
397
+
398
+ try {
399
+ const { WalletFileManager } = await import('./wallet-file.js');
400
+
401
+ // Load wallet from known location: {workspaceRoot}/.descix/wallet.json
402
+ const walletData = await WalletFileManager.loadFromWorkspace(this.workspaceRoot);
403
+ if (!walletData) {
404
+ return null;
405
+ }
406
+
407
+ this.credentials = {
408
+ userId: walletData.userId || null,
409
+ accessToken: walletData.sessionToken || null,
410
+ walletAddress: walletData.walletAddress || null,
411
+ signature: walletData.signature || null,
412
+ communityId: walletData.communityId || null,
413
+ tokenSymbol: walletData.tokenSymbol || null
414
+ };
415
+
416
+ return this.credentials;
417
+ } catch (error) {
418
+ console.error('[ApiClient] Error loading credentials:', error.message);
419
+ return null;
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Set credentials manually
425
+ * @param {Object} credentials - { userId, accessToken, walletAddress, signature, ... }
426
+ */
427
+ setCredentials(credentials) {
428
+ this.credentials = credentials;
429
+ }
430
+
431
+ /**
432
+ * Check if credentials are loaded and valid
433
+ * @returns {boolean}
434
+ */
435
+ hasCredentials() {
436
+ return !!(this.credentials?.userId || this.credentials?.walletAddress);
437
+ }
438
+
439
+ /**
440
+ * Get the PathContext instance (if loaded)
441
+ * Useful for commands that need path resolution
442
+ * @returns {PathContext|null}
443
+ */
444
+ getPathContext() {
445
+ return this._pathContext;
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Create a default API client instance
451
+ * @param {Object} options - Client options
452
+ * @returns {DeSciXApiClient}
453
+ */
454
+ export function createApiClient(options = {}) {
455
+ return new DeSciXApiClient(options);
456
+ }
457
+
458
+ export default DeSciXApiClient;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Authentication Guard
3
+ *
4
+ * Enforces authentication requirement for CLI/MCP commands.
5
+ * Unlike the PWA which allows guest browsing, CLI and MCP require authentication.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+ import { DeSciXApiClient } from './api-client.js';
10
+
11
+ /**
12
+ * Require authentication before executing a command
13
+ * @param {DeSciXApiClient} apiClient - API client instance
14
+ * @returns {Promise<Object>} User information if authenticated
15
+ */
16
+ export async function requireAuth(apiClient) {
17
+ // Load credentials from wallet file
18
+ await apiClient.loadCredentials();
19
+
20
+ if (!apiClient.hasCredentials()) {
21
+ console.error(chalk.red('\n Authentication required.\n'));
22
+ console.error(chalk.white(' Please sign up with DeSciX to use this feature.\n'));
23
+ console.error(chalk.cyan(' Run: descix login\n'));
24
+ process.exit(1);
25
+ }
26
+
27
+ // Validate session is still active by calling check_staked_status via HTTP
28
+ try {
29
+ const response = await apiClient.invoke('check_staked_status', {}, { allowGuest: false });
30
+ if (response.status === 'OK' && response.message) {
31
+ return response.message;
32
+ }
33
+ throw new Error('Invalid session');
34
+ } catch (error) {
35
+ if (error.message.includes('expired') ||
36
+ error.message.includes('invalid') ||
37
+ error.message.includes('Authentication required') ||
38
+ error.message.includes('User session invalid')) {
39
+ console.error(chalk.yellow('\n Session expired or invalid.\n'));
40
+ console.error(chalk.white(' Please sign in again.\n'));
41
+ console.error(chalk.cyan(' Run: descix login\n'));
42
+ process.exit(1);
43
+ }
44
+ // Other errors (network, etc.) - rethrow
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Check if user is authenticated (non-blocking)
51
+ * @param {DeSciXApiClient} apiClient - API client instance
52
+ * @returns {Promise<boolean>} True if authenticated
53
+ */
54
+ export async function isAuthenticated(apiClient) {
55
+ try {
56
+ await apiClient.loadCredentials();
57
+ if (!apiClient.hasCredentials()) {
58
+ return false;
59
+ }
60
+ // Try a lightweight check via HTTP
61
+ const response = await apiClient.invoke('check_staked_status', {}, { allowGuest: false });
62
+ return response.status === 'OK';
63
+ } catch (error) {
64
+ return false;
65
+ }
66
+ }
67
+