@descix/cli 1.0.1 → 1.0.4
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/LICENSE.md +671 -0
- package/README.md +43 -4
- package/agent-assets/guides/README.md +2 -2
- package/agent-assets/guides/dynamic-site-pattern.md +29 -14
- package/agent-assets/guides/knowledge-only-pattern.md +5 -5
- package/agent-assets/guides/microservice-pattern.md +48 -4
- package/agent-assets/guides/static-site-pattern.md +8 -0
- package/agent-assets/instructions/app-asset-validation.md +5 -5
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/instructions/standard-folder-structure.md +20 -11
- package/agent-assets/reference/app-creation.md +42 -20
- package/agent-assets/reference/architecture.md +24 -22
- package/agent-assets/reference/local-dev.md +230 -67
- package/agent-assets/reference/mcp-integration.md +21 -15
- package/agent-assets/reference/platform-api-consumer.md +244 -0
- package/agent-assets/reference/workspace-config.md +7 -7
- package/agent-assets/rules/descix_mcp.template.mdc +8 -8
- package/bin/descix.js +2559 -1435
- package/bin/kb-deny-lint.mjs +91 -0
- package/bin/mcp-server.js +175 -91
- package/lib/agent-files.js +37 -25
- package/lib/api-client.js +223 -56
- package/lib/auth-guard.js +13 -3
- package/lib/chat-session.js +187 -0
- package/lib/cli-version.js +59 -0
- package/lib/commands/airdrop.js +251 -0
- package/lib/commands/auth.js +254 -72
- package/lib/commands/briefer/errors.js +88 -0
- package/lib/commands/briefer/index.js +319 -0
- package/lib/commands/briefer/sources/canonical-sources.js +129 -0
- package/lib/commands/briefer/sources/entitlements.js +220 -0
- package/lib/commands/briefer/sources/environments.js +205 -0
- package/lib/commands/briefer/sources/identifiers.js +214 -0
- package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
- package/lib/commands/briefer/sources/routing.js +275 -0
- package/lib/commands/briefer/sources/what-is-not.js +223 -0
- package/lib/commands/briefer/stitcher.js +134 -0
- package/lib/commands/briefer/util/source-reader.js +502 -0
- package/lib/commands/config.js +210 -90
- package/lib/commands/corpus.js +978 -0
- package/lib/commands/credits.js +122 -0
- package/lib/commands/health.js +627 -0
- package/lib/commands/kb-list-render.js +32 -0
- package/lib/commands/kb.js +190 -413
- package/lib/commands/mcp.js +5 -5
- package/lib/commands/microservice-restart.js +359 -0
- package/lib/commands/model-config.js +313 -0
- package/lib/commands/retired-kb-sync.js +134 -0
- package/lib/commands/serve.js +26 -1
- package/lib/commands/status.js +23 -51
- package/lib/commands/update.js +36 -107
- package/lib/core/Chunker.js +491 -17
- package/lib/core/CorpusDenyLint.js +326 -0
- package/lib/core/CorpusWalker.js +357 -0
- package/lib/core/Hydrator.js +75 -29
- package/lib/core/ManifestLoader.js +533 -0
- package/lib/core/SiteWalker.js +218 -0
- package/lib/core/Syncer.js +163 -11
- package/lib/diagnostic-log.js +41 -0
- package/lib/environment-report.js +100 -0
- package/lib/global-config.js +4 -1
- package/lib/oauth-client.js +164 -0
- package/lib/origin.js +143 -0
- package/lib/service-api-client.js +95 -0
- package/lib/wallet-environment.js +49 -0
- package/lib/wallet-file.js +16 -2
- package/lib/wizard/setup.js +10 -9
- package/lib/workspace-config.js +380 -235
- package/lib/workspace-identity.js +92 -0
- package/lib/workspace-utils.js +41 -61
- package/package.json +19 -6
- package/templates/agent-claude.md +157 -9
- package/templates/agent-cline.md +68 -8
- package/templates/agent-copilot.md +68 -8
- package/templates/agent-cursor.md +68 -8
- package/templates/alignment-schema.json +20 -0
- package/templates/cloud-brain-hook.sh +20 -0
- package/templates/drive/README.md +1 -1
- package/templates/drive_templates/README.md +2 -2
- package/templates/scaffolds/README.md +6 -10
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +104 -149
- package/templates/scaffolds/microservice/app.js +51 -10
- package/templates/scaffolds/microservice/app.yaml +2 -2
- package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
- package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
- package/templates/scaffolds/microservice/manifest.json +0 -1
- package/templates/scaffolds/microservice/package.json +3 -3
- package/templates/scaffolds/microservice/scripts/register.js +56 -41
- package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
- package/templates/scaffolds/microservice/services/utils.js +20 -238
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
- package/lib/commands/app-wizard.js +0 -627
- package/lib/commands/folder.js +0 -291
- package/lib/core/PathContext.js +0 -335
- package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
- package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
- package/templates/scaffolds/site/README.md +0 -51
- package/templates/scaffolds/site/app.js +0 -22
- package/templates/scaffolds/site/index.html +0 -42
- package/templates/scaffolds/site/styles.css +0 -123
package/lib/commands/auth.js
CHANGED
|
@@ -9,11 +9,13 @@ import chalk from 'chalk';
|
|
|
9
9
|
import ora from 'ora';
|
|
10
10
|
import { DeSciXApiClient } from '../api-client.js';
|
|
11
11
|
import { WalletFileManager } from '../wallet-file.js';
|
|
12
|
+
import { walletEnvironmentStamp } from '../wallet-environment.js';
|
|
12
13
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
13
14
|
import { requireAuth } from '../auth-guard.js';
|
|
14
15
|
import { exec } from 'child_process';
|
|
15
16
|
import { promisify } from 'util';
|
|
16
17
|
import * as fs from 'fs/promises';
|
|
18
|
+
import { diag } from '../diagnostic-log.js';
|
|
17
19
|
|
|
18
20
|
const execAsync = promisify(exec);
|
|
19
21
|
const POLL_INTERVAL_MS = 3000; // 3 seconds
|
|
@@ -43,14 +45,47 @@ async function openBrowser(url) {
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* Request device login via HTTP
|
|
48
|
+
* Request device login via HTTP.
|
|
49
|
+
* WS-HEADLESS-MVP-A1: optionally carries the device→OAuth bridge context so the server
|
|
50
|
+
* issues an OAuth authorization code alongside the session on completion (design §2.1).
|
|
47
51
|
* @param {DeSciXApiClient} apiClient - The API client
|
|
52
|
+
* @param {Object|null} oauthCtx - { client_id, code_challenge, redirect_uri, scope }
|
|
48
53
|
*/
|
|
49
|
-
async function requestDeviceLogin(apiClient) {
|
|
50
|
-
const
|
|
54
|
+
async function requestDeviceLogin(apiClient, oauthCtx = null) {
|
|
55
|
+
const params = oauthCtx ? { oauth: oauthCtx } : {};
|
|
56
|
+
const response = await apiClient.invoke('device_request_login', params, { allowGuest: true });
|
|
51
57
|
return response.message;
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/**
|
|
61
|
+
* WS-HEADLESS-MVP-A1 — prepare the OAuth leg of `descix login`:
|
|
62
|
+
* PKCE pair + a registered CLI public client (DCR, reusing a cached client_id when the
|
|
63
|
+
* wallet file already has one). Returns null (with a loud warning) when the server does
|
|
64
|
+
* not expose the OAuth AS — the wallet-sig login proceeds untouched (design §4.1:
|
|
65
|
+
* "Behind a flag; wallet-sig path untouched").
|
|
66
|
+
*/
|
|
67
|
+
async function prepareOAuthLeg(apiClient, existingWallet, scope) {
|
|
68
|
+
const oauthClient = await import('../oauth-client.js');
|
|
69
|
+
const { verifier, challenge } = oauthClient.generatePkcePair();
|
|
70
|
+
|
|
71
|
+
let clientId = existingWallet?.oauth?.client_id || null;
|
|
72
|
+
if (!clientId) {
|
|
73
|
+
clientId = await oauthClient.registerOAuthClient(apiClient.baseUrl, {});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
oauthClient,
|
|
78
|
+
verifier,
|
|
79
|
+
clientId,
|
|
80
|
+
ctx: {
|
|
81
|
+
client_id: clientId,
|
|
82
|
+
code_challenge: challenge,
|
|
83
|
+
redirect_uri: oauthClient.OOB_REDIRECT_URI,
|
|
84
|
+
scope: scope || oauthClient.DEFAULT_OAUTH_SCOPE,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
54
89
|
/**
|
|
55
90
|
* Poll for device login completion via HTTP
|
|
56
91
|
*/
|
|
@@ -69,11 +104,16 @@ async function pollDeviceLogin(apiClient, deviceCode, onStatus = null) {
|
|
|
69
104
|
if (status === 'complete') {
|
|
70
105
|
return {
|
|
71
106
|
user_id: response.message.user_id,
|
|
107
|
+
email: response.message.email || null,
|
|
72
108
|
session_token: response.message.session_token,
|
|
73
109
|
wallet_address: response.message.wallet_address,
|
|
74
110
|
signature: response.message.signature,
|
|
75
|
-
community_id: response.message.community_id
|
|
76
|
-
token_symbol: response.message.token_symbol || 'DAITA'
|
|
111
|
+
community_id: response.message.community_id,
|
|
112
|
+
token_symbol: response.message.token_symbol || 'DAITA',
|
|
113
|
+
// WS-A1 device→OAuth bridge: single-use authorization code (present exactly once
|
|
114
|
+
// when the request carried an oauth ctx) — redeemed at /oauth/token immediately.
|
|
115
|
+
oauth_code: response.message.oauth_code || null,
|
|
116
|
+
oauth_scope: response.message.oauth_scope || null
|
|
77
117
|
};
|
|
78
118
|
}
|
|
79
119
|
|
|
@@ -102,13 +142,13 @@ async function pollDeviceLogin(apiClient, deviceCode, onStatus = null) {
|
|
|
102
142
|
export async function loginDevice(options = {}) {
|
|
103
143
|
const apiClient = new DeSciXApiClient({ baseUrl: options.url });
|
|
104
144
|
|
|
105
|
-
|
|
145
|
+
diag(chalk.cyan('\n🔐 DeSciX Device Login\n'));
|
|
106
146
|
|
|
107
147
|
const spinner = ora('Requesting device login...').start();
|
|
108
148
|
|
|
109
149
|
// Ensure we have the base URL resolved
|
|
110
150
|
await apiClient.ensureBaseUrl();
|
|
111
|
-
|
|
151
|
+
diag(chalk.gray(` API: ${apiClient.baseUrl}\n`));
|
|
112
152
|
|
|
113
153
|
try {
|
|
114
154
|
// Detect workspace root
|
|
@@ -124,19 +164,45 @@ export async function loginDevice(options = {}) {
|
|
|
124
164
|
workspaceRoot = foundRoot || startDir;
|
|
125
165
|
}
|
|
126
166
|
|
|
127
|
-
//
|
|
128
|
-
|
|
167
|
+
// WS-HEADLESS-MVP-A1: OAuth leg (default ON, `--no-oauth` disables). Additive: any
|
|
168
|
+
// failure here warns loudly and the wallet-sig device login proceeds untouched.
|
|
169
|
+
let oauthLeg = null;
|
|
170
|
+
if (options.oauth !== false) {
|
|
171
|
+
try {
|
|
172
|
+
const existingWallet = await WalletFileManager.loadWalletFile(
|
|
173
|
+
WalletFileManager.getProjectWalletPath(workspaceRoot));
|
|
174
|
+
oauthLeg = await prepareOAuthLeg(apiClient, existingWallet, options.scope);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
diag(chalk.yellow(`\n⚠️ OAuth token leg unavailable (${e.message}).`));
|
|
177
|
+
diag(chalk.yellow(' Proceeding with wallet-signature login only.\n'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Request device login (retry ONCE on a stale cached OAuth client_id)
|
|
182
|
+
let request;
|
|
183
|
+
try {
|
|
184
|
+
request = await requestDeviceLogin(apiClient, oauthLeg?.ctx || null);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
if (oauthLeg && /invalid_client/.test(e.message || '')) {
|
|
187
|
+
// Cached client_id no longer registered server-side — re-register and retry.
|
|
188
|
+
oauthLeg.clientId = await oauthLeg.oauthClient.registerOAuthClient(apiClient.baseUrl, {});
|
|
189
|
+
oauthLeg.ctx.client_id = oauthLeg.clientId;
|
|
190
|
+
request = await requestDeviceLogin(apiClient, oauthLeg.ctx);
|
|
191
|
+
} else {
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
129
195
|
|
|
130
196
|
spinner.succeed('Device login request created');
|
|
131
197
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
198
|
+
diag(chalk.yellow('\n📱 Browser will open automatically...\n'));
|
|
199
|
+
diag(chalk.white('In the browser, you will need to:'));
|
|
200
|
+
diag(chalk.gray(' 1. Enter your email address'));
|
|
201
|
+
diag(chalk.gray(' 2. Verify your email with the code sent'));
|
|
202
|
+
diag(chalk.gray(' 3. Accept the Terms of Service'));
|
|
203
|
+
diag(chalk.gray(' 4. Connect your crypto wallet'));
|
|
204
|
+
diag(chalk.gray(' 5. Sign the message to prove wallet ownership'));
|
|
205
|
+
diag('');
|
|
140
206
|
|
|
141
207
|
spinner.start('Opening browser...');
|
|
142
208
|
|
|
@@ -146,10 +212,10 @@ export async function loginDevice(options = {}) {
|
|
|
146
212
|
const opened = await openBrowser(verificationUrl);
|
|
147
213
|
if (opened) {
|
|
148
214
|
spinner.succeed('Browser opened');
|
|
149
|
-
|
|
215
|
+
diag(chalk.yellow(`\nURL: ${verificationUrl}\n`));
|
|
150
216
|
} else {
|
|
151
217
|
spinner.warn('Could not open browser automatically');
|
|
152
|
-
|
|
218
|
+
diag(chalk.yellow(`\nPlease open: ${verificationUrl}\n`));
|
|
153
219
|
}
|
|
154
220
|
|
|
155
221
|
// Poll for completion
|
|
@@ -168,15 +234,45 @@ export async function loginDevice(options = {}) {
|
|
|
168
234
|
userId: credentials.user_id,
|
|
169
235
|
email: credentials.email,
|
|
170
236
|
sessionToken: credentials.session_token,
|
|
171
|
-
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString()
|
|
237
|
+
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
238
|
+
// Record WHERE this credential was obtained. Write-only: nothing refuses on it yet.
|
|
239
|
+
...walletEnvironmentStamp(apiClient.baseUrl)
|
|
172
240
|
};
|
|
173
|
-
|
|
241
|
+
|
|
242
|
+
// WS-HEADLESS-MVP-A1: redeem the device-bridge authorization code for the long-lived
|
|
243
|
+
// OAuth pair (design §3) and cache it gcloud-ADC-style in wallet.json. The AS's auth
|
|
244
|
+
// codes are single-use with a ≤60s TTL — redeem BEFORE anything else.
|
|
245
|
+
if (oauthLeg && credentials.oauth_code) {
|
|
246
|
+
try {
|
|
247
|
+
const tokens = await oauthLeg.oauthClient.redeemAuthorizationCode(apiClient.baseUrl, {
|
|
248
|
+
code: credentials.oauth_code,
|
|
249
|
+
clientId: oauthLeg.clientId,
|
|
250
|
+
codeVerifier: oauthLeg.verifier,
|
|
251
|
+
});
|
|
252
|
+
walletData.oauth = oauthLeg.oauthClient.buildOAuthCredentialBlock({
|
|
253
|
+
clientId: oauthLeg.clientId,
|
|
254
|
+
tokens,
|
|
255
|
+
tokenEndpointBase: apiClient.baseUrl,
|
|
256
|
+
});
|
|
257
|
+
} catch (e) {
|
|
258
|
+
diag(chalk.yellow(`\n⚠️ OAuth code redemption failed: ${e.message}`));
|
|
259
|
+
diag(chalk.yellow(' Wallet-signature login is still valid; re-run "descix login" for OAuth tokens.\n'));
|
|
260
|
+
}
|
|
261
|
+
} else if (oauthLeg && !credentials.oauth_code) {
|
|
262
|
+
diag(chalk.yellow('\n⚠️ Server did not issue an OAuth code (device→OAuth bridge not available).'));
|
|
263
|
+
diag(chalk.yellow(' Wallet-signature login is still valid.\n'));
|
|
264
|
+
}
|
|
265
|
+
|
|
174
266
|
const walletPath = WalletFileManager.getProjectWalletPath(workspaceRoot);
|
|
175
267
|
await WalletFileManager.saveWalletFile(walletPath, walletData);
|
|
176
|
-
|
|
268
|
+
|
|
177
269
|
spinner.succeed(chalk.green('Login successful!'));
|
|
178
|
-
|
|
179
|
-
|
|
270
|
+
if (walletData.oauth) {
|
|
271
|
+
diag(chalk.green(' OAuth tokens cached (30-day silent refresh — gcloud-style).'));
|
|
272
|
+
diag(chalk.gray(` Scope: ${walletData.oauth.scope}`));
|
|
273
|
+
}
|
|
274
|
+
diag(chalk.cyan(`\n✅ Credentials saved to:`));
|
|
275
|
+
diag(chalk.gray(` ${walletPath}\n`));
|
|
180
276
|
|
|
181
277
|
// Sync auto-purchase apps
|
|
182
278
|
spinner.start('Syncing app entitlements...');
|
|
@@ -198,28 +294,28 @@ export async function loginDevice(options = {}) {
|
|
|
198
294
|
}
|
|
199
295
|
} catch (error) {
|
|
200
296
|
spinner.warn(chalk.yellow('Could not sync app entitlements'));
|
|
201
|
-
|
|
297
|
+
diag(chalk.gray(` ${error.message}\n`));
|
|
202
298
|
}
|
|
203
299
|
|
|
204
|
-
|
|
300
|
+
diag(chalk.white('You can now use all DeSciX CLI commands!\n'));
|
|
205
301
|
|
|
206
302
|
} catch (error) {
|
|
207
303
|
spinner.fail(chalk.red('Login failed'));
|
|
208
304
|
console.error(chalk.red(error.message));
|
|
209
305
|
|
|
210
306
|
if (error.message.includes('expired')) {
|
|
211
|
-
|
|
212
|
-
|
|
307
|
+
diag(chalk.yellow('💡 Tip: Device codes expire after 15 minutes.'));
|
|
308
|
+
diag(chalk.yellow(' Run the login command again to get a new code.\n'));
|
|
213
309
|
} else {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
310
|
+
diag(chalk.yellow('💡 Tip: Make sure you:'));
|
|
311
|
+
diag(chalk.yellow(' - Have an internet connection'));
|
|
312
|
+
diag(chalk.yellow(' - Can access the DeSciX platform'));
|
|
313
|
+
diag(chalk.yellow(' - Complete all steps in the browser'));
|
|
314
|
+
diag(chalk.yellow(` - API URL is correct: ${apiClient.baseUrl || 'unknown'}`));
|
|
219
315
|
if (apiClient.baseUrl?.includes('localhost')) {
|
|
220
|
-
|
|
316
|
+
diag(chalk.yellow(' - Local backend server is running'));
|
|
221
317
|
}
|
|
222
|
-
|
|
318
|
+
diag('');
|
|
223
319
|
}
|
|
224
320
|
throw error;
|
|
225
321
|
}
|
|
@@ -229,11 +325,11 @@ export async function loginDevice(options = {}) {
|
|
|
229
325
|
* Direct wallet login command (not yet implemented)
|
|
230
326
|
*/
|
|
231
327
|
export async function loginWallet() {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
328
|
+
diag(chalk.cyan('\n🔐 DeSciX Wallet Login\n'));
|
|
329
|
+
diag(chalk.yellow('\n⚠️ Direct wallet login requires a Web3 provider.\n'));
|
|
330
|
+
diag(chalk.white('For CLI/MCP, device login is recommended:\n'));
|
|
331
|
+
diag(chalk.cyan(' descix login\n'));
|
|
332
|
+
diag(chalk.gray('This opens your browser where you can connect your wallet.\n'));
|
|
237
333
|
throw new Error('Direct wallet login not yet implemented for CLI. Use device login instead.');
|
|
238
334
|
}
|
|
239
335
|
|
|
@@ -284,10 +380,10 @@ export async function reconnect() {
|
|
|
284
380
|
await WalletFileManager.saveWalletFile(walletPath, updatedWalletData);
|
|
285
381
|
|
|
286
382
|
spinner.succeed(chalk.green('Reconnected successfully!'));
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
383
|
+
diag(chalk.cyan(`\n✅ Status: ${result.auth_status || 'CONNECTED'}`));
|
|
384
|
+
diag(chalk.gray(` User: ${sessionInfo?.email || sessionInfo?.id || walletInfo.userId}`));
|
|
385
|
+
diag(chalk.gray(` Wallet: ${walletInfo.walletAddress?.substring(0, 10)}...`));
|
|
386
|
+
diag(chalk.gray(` Session refreshed\n`));
|
|
291
387
|
|
|
292
388
|
} catch (error) {
|
|
293
389
|
spinner.fail(chalk.red('Reconnection failed'));
|
|
@@ -307,7 +403,7 @@ export async function whoami() {
|
|
|
307
403
|
try {
|
|
308
404
|
await requireAuth(apiClient);
|
|
309
405
|
|
|
310
|
-
const response = await apiClient.invoke('
|
|
406
|
+
const response = await apiClient.invoke('validate_session', {}, { allowGuest: false });
|
|
311
407
|
const userInfo = response.message;
|
|
312
408
|
|
|
313
409
|
const walletPath = await WalletFileManager.findWalletFile(process.cwd());
|
|
@@ -316,60 +412,60 @@ export async function whoami() {
|
|
|
316
412
|
|
|
317
413
|
spinner.succeed(chalk.green('Authentication status'));
|
|
318
414
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
415
|
+
diag(chalk.cyan('\n📋 Current Session:\n'));
|
|
416
|
+
diag(chalk.white(` User ID: ${userInfo?.id || userInfo?.user_id || walletInfo?.userId || 'Unknown'}`));
|
|
417
|
+
diag(chalk.white(` Email: ${userInfo?.email || walletInfo?.email || 'N/A'}`));
|
|
322
418
|
if (walletInfo) {
|
|
323
|
-
|
|
419
|
+
diag(chalk.white(` Wallet: ${walletInfo.walletAddress?.substring(0, 10)}...${walletInfo.walletAddress?.slice(-6) || ''}`));
|
|
324
420
|
if (walletInfo.communityId) {
|
|
325
|
-
|
|
421
|
+
diag(chalk.white(` Community: ${walletInfo.communityId}`));
|
|
326
422
|
}
|
|
327
423
|
if (walletInfo.tokenSymbol) {
|
|
328
|
-
|
|
424
|
+
diag(chalk.white(` Token: ${walletInfo.tokenSymbol}`));
|
|
329
425
|
}
|
|
330
|
-
|
|
426
|
+
diag(chalk.white(` Session: ${isValid ? chalk.green('Valid') : chalk.red('Expired')}`));
|
|
331
427
|
|
|
332
428
|
if (walletInfo.expiresAt) {
|
|
333
429
|
const expiresDate = new Date(walletInfo.expiresAt);
|
|
334
430
|
const now = new Date();
|
|
335
431
|
if (expiresDate > now) {
|
|
336
432
|
const hoursLeft = Math.round((expiresDate - now) / (1000 * 60 * 60));
|
|
337
|
-
|
|
433
|
+
diag(chalk.white(` Expires: ${hoursLeft}h remaining`));
|
|
338
434
|
} else {
|
|
339
|
-
|
|
435
|
+
diag(chalk.white(` Expires: ${chalk.red('Expired')}`));
|
|
340
436
|
}
|
|
341
437
|
}
|
|
342
438
|
}
|
|
343
439
|
|
|
344
440
|
if (userInfo) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
441
|
+
// Account Details: feature access is entitlement-based (community membership / subscriptions),
|
|
442
|
+
// NOT a custodial balance or a STAKED state — both removed per CEO-D-2026-06-14-DROP-CUSTODIAL-ELIMINATE-STAKED.
|
|
443
|
+
diag(chalk.cyan('\n📊 Account Details:\n'));
|
|
444
|
+
|
|
349
445
|
try {
|
|
350
446
|
const purchasesResponse = await apiClient.invoke('fetch_my_purchases', { product_type: 'APP' }, { allowGuest: false });
|
|
351
447
|
const purchases = purchasesResponse.message || {};
|
|
352
448
|
const communities = purchases.communities || [];
|
|
353
449
|
const apps = purchases.apps || purchases.products || [];
|
|
354
450
|
|
|
355
|
-
|
|
356
|
-
|
|
451
|
+
diag(chalk.white(` Communities: ${communities.length}`));
|
|
452
|
+
diag(chalk.white(` Apps: ${apps.length}`));
|
|
357
453
|
if (apps.length > 0) {
|
|
358
454
|
apps.slice(0, 5).forEach(a => {
|
|
359
455
|
const id = a.app_id || a.appId || a.id || a.product_id;
|
|
360
|
-
if (id)
|
|
456
|
+
if (id) diag(chalk.gray(` - ${id}`));
|
|
361
457
|
});
|
|
362
|
-
if (apps.length > 5)
|
|
458
|
+
if (apps.length > 5) diag(chalk.gray(` ... and ${apps.length - 5} more`));
|
|
363
459
|
}
|
|
364
460
|
} catch (err) {
|
|
365
461
|
// Ignore purchase fetch errors
|
|
366
462
|
}
|
|
367
463
|
}
|
|
368
464
|
|
|
369
|
-
|
|
465
|
+
diag('');
|
|
370
466
|
|
|
371
467
|
if (walletInfo && !isValid) {
|
|
372
|
-
|
|
468
|
+
diag(chalk.yellow(' ⚠️ Session expired. Run "descix reconnect" to refresh.\n'));
|
|
373
469
|
}
|
|
374
470
|
|
|
375
471
|
} catch (error) {
|
|
@@ -380,28 +476,114 @@ export async function whoami() {
|
|
|
380
476
|
}
|
|
381
477
|
|
|
382
478
|
/**
|
|
383
|
-
* Logout - clear
|
|
479
|
+
* Logout - revoke server-side OAuth credentials (RFC 7009), then clear the local cache.
|
|
480
|
+
* WS-HEADLESS-MVP-A1 (design §4.6): revocation kills the whole refresh-token chain so a
|
|
481
|
+
* copied wallet.json cannot keep refreshing after logout. Revocation failure is reported
|
|
482
|
+
* but never blocks the local credential wipe.
|
|
384
483
|
*/
|
|
385
484
|
export async function logout() {
|
|
386
485
|
const spinner = ora('Logging out...').start();
|
|
387
|
-
|
|
486
|
+
|
|
388
487
|
try {
|
|
389
488
|
const walletPath = await WalletFileManager.findWalletFile(process.cwd());
|
|
390
|
-
|
|
489
|
+
|
|
391
490
|
if (!walletPath) {
|
|
392
491
|
spinner.info(chalk.yellow('Not logged in'));
|
|
393
|
-
|
|
492
|
+
diag(chalk.gray('\n No credentials to remove.\n'));
|
|
394
493
|
return;
|
|
395
494
|
}
|
|
396
|
-
|
|
495
|
+
|
|
496
|
+
const walletInfo = await WalletFileManager.loadWalletFile(walletPath);
|
|
497
|
+
if (walletInfo?.oauth?.refresh_token) {
|
|
498
|
+
spinner.text = 'Revoking OAuth credentials...';
|
|
499
|
+
try {
|
|
500
|
+
const apiClient = new DeSciXApiClient();
|
|
501
|
+
await apiClient.ensureBaseUrl();
|
|
502
|
+
const { revokeOAuthToken } = await import('../oauth-client.js');
|
|
503
|
+
await revokeOAuthToken(apiClient.baseUrl, {
|
|
504
|
+
token: walletInfo.oauth.refresh_token,
|
|
505
|
+
clientId: walletInfo.oauth.client_id,
|
|
506
|
+
});
|
|
507
|
+
diag(chalk.gray('\n OAuth refresh-token chain revoked server-side.'));
|
|
508
|
+
} catch (e) {
|
|
509
|
+
diag(chalk.yellow(`\n ⚠️ OAuth revocation failed (${e.message}); removing local credentials anyway.`));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
397
513
|
await fs.unlink(walletPath);
|
|
398
|
-
|
|
514
|
+
|
|
399
515
|
spinner.succeed(chalk.green('Logged out'));
|
|
400
|
-
|
|
401
|
-
|
|
516
|
+
diag(chalk.cyan('\n✅ Credentials removed\n'));
|
|
517
|
+
|
|
402
518
|
} catch (error) {
|
|
403
519
|
spinner.fail(chalk.red('Logout failed'));
|
|
404
520
|
console.error(chalk.red(error.message));
|
|
405
521
|
throw error;
|
|
406
522
|
}
|
|
407
523
|
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Admin bootstrap login — generates CLI credentials for platform admins
|
|
527
|
+
* without requiring the PWA device login / Powch flow.
|
|
528
|
+
* Requires email to be in the DESCIX_ADMIN_GROUP Google Group.
|
|
529
|
+
*/
|
|
530
|
+
export async function adminLogin(options = {}) {
|
|
531
|
+
const email = options.email;
|
|
532
|
+
if (!email) {
|
|
533
|
+
console.error(chalk.red('--email is required'));
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const spinner = ora('Authenticating as admin...').start();
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const apiClient = new DeSciXApiClient();
|
|
541
|
+
await apiClient.ensureBaseUrl();
|
|
542
|
+
|
|
543
|
+
// admin_bootstrap_login is a bootstrap command: it must be sent guest-like,
|
|
544
|
+
// never carrying a previously-stored access_token. Targeting a DIFFERENT env
|
|
545
|
+
// than the one that minted the stored token (e.g. CLI configured for dev but
|
|
546
|
+
// logging into prod via --env prod) would otherwise send a token prod's auth
|
|
547
|
+
// middleware rejects with HTTP 401 BEFORE admin_bootstrap_login runs. Force
|
|
548
|
+
// guest by skipping the session token, same as device_request_login.
|
|
549
|
+
const result = await apiClient.invokeRaw('admin_bootstrap_login', { email }, null, { skipSessionCheck: true });
|
|
550
|
+
const data = result.data || result;
|
|
551
|
+
|
|
552
|
+
if (data.status === 'ERROR') {
|
|
553
|
+
spinner.fail(data.message || 'Admin login failed');
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const info = data.message;
|
|
558
|
+
if (!info || !info.session_token) {
|
|
559
|
+
spinner.fail('Unexpected response from server');
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const walletData = {
|
|
564
|
+
walletAddress: info.wallet_address,
|
|
565
|
+
signature: info.signature,
|
|
566
|
+
sessionToken: info.session_token,
|
|
567
|
+
userId: info.user_id,
|
|
568
|
+
email: info.email,
|
|
569
|
+
communityId: info.community_id,
|
|
570
|
+
tokenSymbol: info.token_symbol || 'DAITA',
|
|
571
|
+
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
|
|
572
|
+
// Record WHERE this credential was obtained. Write-only: nothing refuses on it yet.
|
|
573
|
+
...walletEnvironmentStamp(apiClient.baseUrl),
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// Save wallet.json
|
|
577
|
+
const workspaceRoot = await WorkspaceConfig.findWorkspaceRoot(process.cwd()) || process.cwd();
|
|
578
|
+
const walletPath = WalletFileManager.getProjectWalletPath(workspaceRoot);
|
|
579
|
+
await WalletFileManager.saveWalletFile(walletPath, walletData);
|
|
580
|
+
|
|
581
|
+
spinner.succeed(chalk.green(`Logged in as admin: ${email}`));
|
|
582
|
+
diag(chalk.dim(` Wallet: ${walletData.walletAddress}`));
|
|
583
|
+
diag(chalk.dim(` Session expires: ${walletData.expiresAt}`));
|
|
584
|
+
diag(chalk.dim(` Credentials saved to: ${walletPath}`));
|
|
585
|
+
} catch (error) {
|
|
586
|
+
spinner.fail(error.message || 'Admin login failed');
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrieferExtractorError — canonical hard-fail surface for the `descix briefer` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Per WS-DESCIX-BRIEFER-CLI scope doc §2.3 ("Hard-fail philosophy"), every data
|
|
5
|
+
* source has exactly one extraction method and exactly one failure mode:
|
|
6
|
+
* HARD-FAIL with an actionable error message.
|
|
7
|
+
*
|
|
8
|
+
* Per feedback_no-hardcoded-fallbacks: NO fallback values, NO best-effort prose,
|
|
9
|
+
* NO try-catch-and-default. If a source moved or changed shape, the CLI exits
|
|
10
|
+
* non-zero and the canonical briefer file remains untouched.
|
|
11
|
+
*
|
|
12
|
+
* Stable error codes (per scope doc §2.3) — grep-friendly for diagnostics:
|
|
13
|
+
* BRIEFER-SRC-NOT-FOUND Source file missing / line range no longer matches
|
|
14
|
+
* BRIEFER-GCLOUD-AUTH gcloud auth missing or insufficient scope
|
|
15
|
+
* BRIEFER-GCLOUD-FAIL gcloud probe failed (auth/empty/timeout) when --env=demo|prod required it
|
|
16
|
+
* BRIEFER-PARSE-FAIL Source file present but construct unparseable
|
|
17
|
+
* BRIEFER-DRIFT-DETECTED --check diff against canonical briefer non-empty
|
|
18
|
+
* BRIEFER-NEGATIVE-CLAIM Briefer claims X does not exist, but found at Y:Z
|
|
19
|
+
* BRIEFER-FIRESTORE Firestore read via apifront failed
|
|
20
|
+
* BRIEFER-NOT-IMPLEMENTED M1 stub — extractor scaffold-only, real impl in later milestone
|
|
21
|
+
*
|
|
22
|
+
* Per scope doc §2.3, every error message must include:
|
|
23
|
+
* 1. The exact source path + line range that was being read.
|
|
24
|
+
* 2. The construct shape that was expected.
|
|
25
|
+
* 3. What the operator should do to recover.
|
|
26
|
+
* 4. A stable error code from the list above.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export const BRIEFER_ERROR_CODES = Object.freeze({
|
|
30
|
+
SRC_NOT_FOUND: 'BRIEFER-SRC-NOT-FOUND',
|
|
31
|
+
GCLOUD_AUTH: 'BRIEFER-GCLOUD-AUTH',
|
|
32
|
+
PARSE_FAIL: 'BRIEFER-PARSE-FAIL',
|
|
33
|
+
DRIFT_DETECTED: 'BRIEFER-DRIFT-DETECTED',
|
|
34
|
+
NEGATIVE_CLAIM: 'BRIEFER-NEGATIVE-CLAIM',
|
|
35
|
+
FIRESTORE: 'BRIEFER-FIRESTORE',
|
|
36
|
+
GCLOUD_FAIL: 'BRIEFER-GCLOUD-FAIL',
|
|
37
|
+
NOT_IMPLEMENTED: 'BRIEFER-NOT-IMPLEMENTED',
|
|
38
|
+
WORKSPACE_NOT_FOUND: 'BRIEFER-WORKSPACE-NOT-FOUND'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export class BrieferExtractorError extends Error {
|
|
42
|
+
/**
|
|
43
|
+
* @param {object} args
|
|
44
|
+
* @param {string} args.code One of BRIEFER_ERROR_CODES (REQUIRED).
|
|
45
|
+
* @param {string} args.section Briefer section being extracted (e.g., '§3 routing').
|
|
46
|
+
* @param {string} args.source File path or 'gcloud' or 'firestore' (REQUIRED).
|
|
47
|
+
* @param {string} args.expected Construct shape that was expected (REQUIRED).
|
|
48
|
+
* @param {string} args.recovery Concrete next step for the operator (REQUIRED).
|
|
49
|
+
* @param {string} [args.detail] Optional extra context (e.g., parser msg, exit code).
|
|
50
|
+
*/
|
|
51
|
+
constructor({ code, section, source, expected, recovery, detail } = {}) {
|
|
52
|
+
if (!code || !Object.values(BRIEFER_ERROR_CODES).includes(code)) {
|
|
53
|
+
// Hard-fail loud on misuse — caller is constructing a bad error.
|
|
54
|
+
// We do NOT default the code; per feedback_no-hardcoded-fallbacks the
|
|
55
|
+
// absence of a code is itself a misconfiguration we want to surface.
|
|
56
|
+
throw new Error(
|
|
57
|
+
`BrieferExtractorError requires a valid 'code'. Got: ${JSON.stringify(code)}. ` +
|
|
58
|
+
`Valid codes: ${Object.values(BRIEFER_ERROR_CODES).join(', ')}.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!source) {
|
|
62
|
+
throw new Error(`BrieferExtractorError requires a 'source' (file path / 'gcloud' / 'firestore').`);
|
|
63
|
+
}
|
|
64
|
+
if (!expected) {
|
|
65
|
+
throw new Error(`BrieferExtractorError requires an 'expected' construct shape description.`);
|
|
66
|
+
}
|
|
67
|
+
if (!recovery) {
|
|
68
|
+
throw new Error(`BrieferExtractorError requires a 'recovery' instruction for the operator.`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = [
|
|
72
|
+
`[${code}] ${section || '(section unspecified)'}`,
|
|
73
|
+
` source: ${source}`,
|
|
74
|
+
` expected: ${expected}`,
|
|
75
|
+
` recovery: ${recovery}`
|
|
76
|
+
];
|
|
77
|
+
if (detail) lines.push(` detail: ${detail}`);
|
|
78
|
+
|
|
79
|
+
super(lines.join('\n'));
|
|
80
|
+
this.name = 'BrieferExtractorError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.section = section || null;
|
|
83
|
+
this.source = source;
|
|
84
|
+
this.expected = expected;
|
|
85
|
+
this.recovery = recovery;
|
|
86
|
+
this.detail = detail || null;
|
|
87
|
+
}
|
|
88
|
+
}
|