@descix/cli 1.0.6 → 1.0.8
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 +1 -1
- package/agent-assets/guides/README.md +1 -1
- package/agent-assets/guides/dynamic-site-pattern.md +17 -30
- package/agent-assets/guides/knowledge-only-pattern.md +11 -15
- package/agent-assets/guides/microservice-pattern.md +12 -14
- package/agent-assets/guides/static-site-pattern.md +12 -15
- package/agent-assets/instructions/app-asset-validation.md +1 -1
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/reference/app-creation.md +36 -101
- package/agent-assets/reference/architecture.md +43 -51
- package/agent-assets/reference/local-dev.md +1 -6
- package/agent-assets/reference/mcp-integration.md +21 -23
- package/agent-assets/reference/workspace-config.md +97 -340
- package/bin/descix.js +337 -618
- package/lib/api-client.js +30 -22
- package/lib/command-visibility.js +213 -0
- package/lib/commands/airdrop.js +10 -0
- package/lib/commands/auth.js +16 -2
- package/lib/commands/config.js +13 -31
- package/lib/commands/corpus.js +163 -59
- package/lib/commands/health.js +116 -21
- package/lib/commands/init.js +17 -45
- package/lib/commands/kb.js +7 -0
- package/lib/commands/retired-kb-sync.js +5 -27
- package/lib/commands/status.js +0 -9
- package/lib/core/Chunker.js +1 -1
- package/lib/core/CorpusWalker.js +238 -47
- package/lib/core/ManifestLoader.js +68 -0
- package/lib/core/Syncer.js +147 -46
- package/lib/interactive.js +158 -0
- package/lib/origin.js +3 -3
- package/lib/sdk-assets.js +28 -0
- package/lib/verb-invokes.generated.json +233 -0
- package/lib/wallet-file.js +36 -37
- package/lib/workspace-config.js +266 -199
- package/lib/workspace-identity.js +0 -1
- package/package.json +7 -3
- package/templates/agent-claude.md +1 -3
- package/templates/agent-cline.md +1 -2
- package/templates/agent-copilot.md +1 -2
- package/templates/agent-cursor.md +1 -2
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +1 -1
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +2 -3
- package/agent-assets/rules/descix_mcp.template.mdc +0 -243
- package/lib/commands/briefer/errors.js +0 -88
- package/lib/commands/briefer/index.js +0 -319
- package/lib/commands/briefer/sources/canonical-sources.js +0 -129
- package/lib/commands/briefer/sources/entitlements.js +0 -220
- package/lib/commands/briefer/sources/environments.js +0 -205
- package/lib/commands/briefer/sources/identifiers.js +0 -214
- package/lib/commands/briefer/sources/microservice-deploy.js +0 -267
- package/lib/commands/briefer/sources/routing.js +0 -275
- package/lib/commands/briefer/sources/what-is-not.js +0 -223
- package/lib/commands/briefer/stitcher.js +0 -134
- package/lib/commands/briefer/util/source-reader.js +0 -502
- package/lib/commands/mcp.js +0 -273
- package/lib/commands/update.js +0 -581
- package/lib/wizard/setup.js +0 -503
- package/templates/descix_mcp.mdc +0 -122
package/lib/api-client.js
CHANGED
|
@@ -13,6 +13,35 @@ import { resolveOrigin } from './origin.js';
|
|
|
13
13
|
import { reportEnvironment, EXPLICIT_ORIGIN_SOURCE } from './environment-report.js';
|
|
14
14
|
import { diag } from './diagnostic-log.js';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* THE effective API origin for this invocation, with the source that chose it: the environment
|
|
18
|
+
* variable (which the --api-url / --env flags fold into), then the workspace's env.apiUrl, then
|
|
19
|
+
* ~/.descix/config.json, then the declared default. Every surface that SHOWS or USES the origin
|
|
20
|
+
* consumes this — `config show` used to print the workspace file's value while every command
|
|
21
|
+
* used the environment variable's, so an inherited DESCIX_API_URL silently pointed commands at
|
|
22
|
+
* production while `config show` said dev (JARVIS-FRAQTL, 2026-09-16).
|
|
23
|
+
*
|
|
24
|
+
* Never returns null and never throws for an ABSENT configuration (nothing configured is the
|
|
25
|
+
* declared default, carrying `source: default`, which the caller prints). Throws
|
|
26
|
+
* OriginInvalidError when a source named an origin that cannot be used.
|
|
27
|
+
*
|
|
28
|
+
* @param {string|null|undefined} workspaceEnvApiUrl - the loaded workspace's env.apiUrl
|
|
29
|
+
* @returns {Promise<{origin: string, source: string, isDefault: boolean}>}
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveEffectiveOrigin(workspaceEnvApiUrl) {
|
|
32
|
+
let globalApiUrl = null;
|
|
33
|
+
try {
|
|
34
|
+
const { GlobalConfig } = await import('./global-config.js');
|
|
35
|
+
const gc = await GlobalConfig.load();
|
|
36
|
+
globalApiUrl = gc.api_url || null;
|
|
37
|
+
} catch {}
|
|
38
|
+
return resolveOrigin({
|
|
39
|
+
envVar: process.env.DESCIX_API_URL,
|
|
40
|
+
workspaceEnvApiUrl,
|
|
41
|
+
globalApiUrl,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
16
45
|
export class DeSciXApiClient {
|
|
17
46
|
constructor(options = {}) {
|
|
18
47
|
this.baseUrl = options.baseUrl || null; // Will be loaded async
|
|
@@ -93,28 +122,7 @@ export class DeSciXApiClient {
|
|
|
93
122
|
* @returns {Promise<{origin: string, source: string, isDefault: boolean}>}
|
|
94
123
|
*/
|
|
95
124
|
async detectApiUrl() {
|
|
96
|
-
|
|
97
|
-
// developer having chosen it, and the `!== PRODUCTION_URL` comparisons made it WORSE than a
|
|
98
|
-
// plain fallback: a workspace that had deliberately set prod was treated as if it had set
|
|
99
|
-
// nothing and skipped, so "chosen" and "unconfigured" were provably indistinguishable. The
|
|
100
|
-
// precedence and the fail-loud now live in the one origin owner.
|
|
101
|
-
let globalApiUrl = null;
|
|
102
|
-
try {
|
|
103
|
-
const { GlobalConfig } = await import('./global-config.js');
|
|
104
|
-
const gc = await GlobalConfig.load();
|
|
105
|
-
globalApiUrl = gc.api_url || null;
|
|
106
|
-
} catch {}
|
|
107
|
-
|
|
108
|
-
// Never returns null and never throws for an ABSENT configuration: nothing configured is
|
|
109
|
-
// the declared default (PROD), carrying `source: default`, which the caller prints. It DOES
|
|
110
|
-
// throw OriginInvalidError when a source named an origin that cannot be used — silently
|
|
111
|
-
// discarding what the developer typed is the same defect from the other direction.
|
|
112
|
-
return resolveOrigin({
|
|
113
|
-
envVar: process.env.DESCIX_API_URL,
|
|
114
|
-
workspaceEnvApiUrl: this._workspaceConfig?.env?.apiUrl,
|
|
115
|
-
legacyApiUrl: this._workspaceConfig?.apiUrl,
|
|
116
|
-
globalApiUrl,
|
|
117
|
-
});
|
|
125
|
+
return resolveEffectiveOrigin(this._workspaceConfig?.env?.apiUrl);
|
|
118
126
|
}
|
|
119
127
|
|
|
120
128
|
/**
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Visibility — the PUBLIC-vs-admin CLI help listing.
|
|
3
|
+
*
|
|
4
|
+
* CEO ruling (verbatim): "The microservice manifest consumed by DeSciX core should determine
|
|
5
|
+
* what gets exposed via MCP (and therefore CLI listing should be easily exposed there too for
|
|
6
|
+
* the core itself) - shouldn't we reuse that surface." And: "It's not a worry that admin
|
|
7
|
+
* commands can be discovered by normal users but rather we are trying to avoid the flood of
|
|
8
|
+
* noise so it is acceptable to have two lists that can be derived from the same manifest."
|
|
9
|
+
*
|
|
10
|
+
* THE CLASS COMES FROM THE SERVER AT RUNTIME (`get_command_surface`, guest-allowed) — there is
|
|
11
|
+
* NO committed class table in this repo. The only thing committed here is
|
|
12
|
+
* lib/verb-invokes.generated.json, a CLI-owned fact ("this verb invokes these commands"),
|
|
13
|
+
* regenerated by scripts/generate-verb-invokes.mjs from bin/descix.js + lib/** itself.
|
|
14
|
+
*
|
|
15
|
+
* A verb is hidden from the default listing when it invokes at least one command AND every
|
|
16
|
+
* command it invokes is classified 'admin' by the server. A verb with no recorded invokes
|
|
17
|
+
* (local-only, or a verb whose only invoke call sites are dynamic — see
|
|
18
|
+
* scripts/lib/verb-invoke-graph.mjs) stays visible: absence of information is not evidence of
|
|
19
|
+
* admin-only-ness. A GROUP (a command with children and no action of its own) is hidden when
|
|
20
|
+
* every one of its children ends up hidden — hiding is transitive up the tree, computed
|
|
21
|
+
* bottom-up from the LIVE commander tree (never a second, hand-kept tree).
|
|
22
|
+
*
|
|
23
|
+
* Hiding is LISTING ONLY. A hidden verb still EXECUTES normally when typed; the server's own
|
|
24
|
+
* permission gate (checkCommandPermission) is the actual enforcement, unchanged by this file.
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'fs/promises';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import os from 'os';
|
|
29
|
+
import VERB_INVOKES from './verb-invokes.generated.json' with { type: 'json' };
|
|
30
|
+
|
|
31
|
+
const CACHE_PATH = path.join(os.homedir(), '.descix', 'command-surface-cache.json');
|
|
32
|
+
/** Short TTL: this is a LISTING convenience cache, not a security boundary — a stale read only
|
|
33
|
+
* ever risks showing (or hiding) a help row for a few minutes, never changes what executes. */
|
|
34
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ONE explicit, CLI-owned declaration: Drive content authoring (`descix drive pull/push`) needs
|
|
38
|
+
* infrastructure admin credentials (Google ADC / a Drive service account) that `get_command_surface`
|
|
39
|
+
* has no visibility into — it is not gated by any COMMAND_PERMISSIONS entry the server classifies
|
|
40
|
+
* 'admin', so it can never be derived from the manifest. Per the CEO's ruling these stay OUT of
|
|
41
|
+
* the public CLI listing (internal Drive authoring still uses them — hidden, never deleted). The
|
|
42
|
+
* `drive` group itself is included for clarity; it would also be hidden automatically once both
|
|
43
|
+
* of its children are (see applyVisibility's group rule), but naming it here keeps this ONE set
|
|
44
|
+
* the single place that answers "why is drive hidden" rather than splitting the answer across a
|
|
45
|
+
* declaration and an inference.
|
|
46
|
+
*/
|
|
47
|
+
export const INFRA_ADMIN_HIDDEN_PATHS = new Set(['drive', 'drive pull', 'drive push']);
|
|
48
|
+
|
|
49
|
+
/** @returns {Record<string,string[]>} the committed, generated verb -> invoked-commands map. */
|
|
50
|
+
export function getVerbInvokeMap() {
|
|
51
|
+
return VERB_INVOKES;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetch the server's per-command visibility surface as a guest (no login needed).
|
|
56
|
+
* @param {import('./api-client.js').DeSciXApiClient} apiClient
|
|
57
|
+
* @returns {Promise<{ service: string, manifest_hash: string, commands: Record<string,'public'|'admin'> }>}
|
|
58
|
+
*/
|
|
59
|
+
export async function fetchCommandSurface(apiClient) {
|
|
60
|
+
const resp = await apiClient.invoke('get_command_surface', {}, { allowGuest: true });
|
|
61
|
+
const msg = resp?.message || resp;
|
|
62
|
+
if (!msg || typeof msg.commands !== 'object' || msg.commands === null) {
|
|
63
|
+
throw new Error(`get_command_surface returned an unexpected shape: ${JSON.stringify(resp)}`);
|
|
64
|
+
}
|
|
65
|
+
return msg;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function readCache() {
|
|
69
|
+
try {
|
|
70
|
+
const raw = await fs.readFile(CACHE_PATH, 'utf8');
|
|
71
|
+
return JSON.parse(raw);
|
|
72
|
+
} catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function writeCache(cache) {
|
|
78
|
+
try {
|
|
79
|
+
await fs.mkdir(path.dirname(CACHE_PATH), { recursive: true });
|
|
80
|
+
await fs.writeFile(CACHE_PATH, JSON.stringify(cache, null, 2), 'utf8');
|
|
81
|
+
} catch {
|
|
82
|
+
// Best-effort. A cache write failure must never break help output — the caller already has
|
|
83
|
+
// the freshly-fetched surface in hand and can render with it regardless.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The server surface for `origin`, from the user-level cache (keyed by origin, short TTL) when
|
|
89
|
+
* fresh, else fetched live and cached. Throws if the fetch fails (network down, old server
|
|
90
|
+
* without the command, etc.) — the caller decides what "help shows everything" means.
|
|
91
|
+
*
|
|
92
|
+
* @param {string} origin
|
|
93
|
+
* @param {import('./api-client.js').DeSciXApiClient} apiClient
|
|
94
|
+
* @returns {Promise<{ commands: Record<string,'public'|'admin'>, fromCache: boolean }>}
|
|
95
|
+
*/
|
|
96
|
+
export async function getCommandSurface(origin, apiClient) {
|
|
97
|
+
const cache = await readCache();
|
|
98
|
+
const entry = cache[origin];
|
|
99
|
+
if (entry && typeof entry.fetchedAt === 'number' && (Date.now() - entry.fetchedAt) < CACHE_TTL_MS) {
|
|
100
|
+
return { commands: entry.surface.commands, fromCache: true };
|
|
101
|
+
}
|
|
102
|
+
const surface = await fetchCommandSurface(apiClient);
|
|
103
|
+
cache[origin] = { surface, fetchedAt: Date.now() };
|
|
104
|
+
await writeCache(cache);
|
|
105
|
+
return { commands: surface.commands, fromCache: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Should a single LEAF verb (identified by its full space-joined path, e.g. "kb corpus sync") be
|
|
110
|
+
* hidden from the default listing, given the server's command->visibility surface?
|
|
111
|
+
*
|
|
112
|
+
* @param {string} verbPath
|
|
113
|
+
* @param {Record<string,'public'|'admin'>} surfaceCommands
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
export function shouldHideLeaf(verbPath, surfaceCommands) {
|
|
117
|
+
if (INFRA_ADMIN_HIDDEN_PATHS.has(verbPath)) return true;
|
|
118
|
+
const invokes = VERB_INVOKES[verbPath];
|
|
119
|
+
if (!invokes || invokes.length === 0) return false; // no recorded invokes -> stays visible
|
|
120
|
+
return invokes.every((cmd) => surfaceCommands[cmd] === 'admin');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Apply hide/show to the LIVE commander tree rooted at `program`, bottom-up. Never reads or
|
|
125
|
+
* builds a second command tree — `cmd.commands` / `cmd._actionHandler` (commander's own runtime
|
|
126
|
+
* state) is the only source of "what verbs exist and how they nest", so this can never drift
|
|
127
|
+
* from the tree `--help` actually renders.
|
|
128
|
+
*
|
|
129
|
+
* @param {import('commander').Command} program
|
|
130
|
+
* @param {Record<string,'public'|'admin'>} surfaceCommands
|
|
131
|
+
*/
|
|
132
|
+
export function applyVisibility(program, surfaceCommands) {
|
|
133
|
+
function visit(cmd, pathTokens) {
|
|
134
|
+
const verbPath = pathTokens.join(' ');
|
|
135
|
+
const hasChildren = Array.isArray(cmd.commands) && cmd.commands.length > 0;
|
|
136
|
+
// ADDITIVE ONLY. Some commands are registered `{ hidden: true }` by the CLI itself for
|
|
137
|
+
// reasons that have nothing to do with server admin-gating — see the retired-surface
|
|
138
|
+
// registrations in lib/commands/retired-kb-sync.js, each a deliberate loud-refusal stub kept
|
|
139
|
+
// out of the default listing so it doesn't clutter it, while still working and refusing
|
|
140
|
+
// clearly when typed. This layer may HIDE a visible command; it must never REVEAL a command
|
|
141
|
+
// the CLI already chose to hide. MEASURED: an early version unconditionally overwrote
|
|
142
|
+
// `_hidden`, which un-hid a retired-surface stub the moment its verb had no generated-map
|
|
143
|
+
// entry (it invokes no /apifront command at all, so its invoke list is empty).
|
|
144
|
+
const alreadyHidden = !!cmd._hidden;
|
|
145
|
+
|
|
146
|
+
if (hasChildren) {
|
|
147
|
+
let allChildrenHidden = true;
|
|
148
|
+
for (const child of cmd.commands) {
|
|
149
|
+
const childHidden = visit(child, [...pathTokens, child.name()]);
|
|
150
|
+
if (!childHidden) allChildrenHidden = false;
|
|
151
|
+
}
|
|
152
|
+
if (pathTokens.length === 0) return false; // never hide the root program
|
|
153
|
+
if (cmd._actionHandler) {
|
|
154
|
+
// A command that is BOTH a group and its own runnable leaf (not used in this CLI today,
|
|
155
|
+
// but commander allows it) is judged as a leaf: its own invokes decide it, independent
|
|
156
|
+
// of its children.
|
|
157
|
+
const hidden = alreadyHidden || shouldHideLeaf(verbPath, surfaceCommands);
|
|
158
|
+
cmd._hidden = hidden;
|
|
159
|
+
return hidden;
|
|
160
|
+
}
|
|
161
|
+
cmd._hidden = alreadyHidden || allChildrenHidden;
|
|
162
|
+
return cmd._hidden;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (pathTokens.length === 0) return false; // the root has no children in practice, but guard anyway
|
|
166
|
+
const hidden = alreadyHidden || shouldHideLeaf(verbPath, surfaceCommands);
|
|
167
|
+
cmd._hidden = hidden;
|
|
168
|
+
return hidden;
|
|
169
|
+
}
|
|
170
|
+
visit(program, []);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Is this invocation one that will render `--help` output? Only these fetch the network — every
|
|
175
|
+
* other invocation must stay fast and offline-safe.
|
|
176
|
+
*
|
|
177
|
+
* Recognizes: `-h` / `--help` anywhere, `help` in subcommand position, a bare invocation (no positional tokens once
|
|
178
|
+
* global flags are stripped), and a "bare group" invocation (the resolved command has children
|
|
179
|
+
* but no action handler of its own — e.g. `descix app` with no subcommand, which commander
|
|
180
|
+
* renders as help even though the CLI never called outputHelp() itself).
|
|
181
|
+
*
|
|
182
|
+
* @param {import('commander').Command} program
|
|
183
|
+
* @param {string[]} argv - process.argv.slice(2)
|
|
184
|
+
* @param {{ valueFlags?: string[], booleanFlags?: string[] }} [globalFlags]
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
export function isHelpInvocation(program, argv, globalFlags = {}) {
|
|
188
|
+
if (argv.length === 0) return true;
|
|
189
|
+
if (argv.includes('-h') || argv.includes('--help')) return true;
|
|
190
|
+
if (argv.includes('-V') || argv.includes('--version')) return false; // prints a version, renders no listing
|
|
191
|
+
|
|
192
|
+
const valueFlags = new Set(globalFlags.valueFlags || []);
|
|
193
|
+
const booleanFlags = new Set(globalFlags.booleanFlags || []);
|
|
194
|
+
const positional = [];
|
|
195
|
+
for (let i = 0; i < argv.length; i++) {
|
|
196
|
+
const tok = argv[i];
|
|
197
|
+
if (valueFlags.has(tok)) { i++; continue; }
|
|
198
|
+
if (booleanFlags.has(tok)) continue;
|
|
199
|
+
if (tok.startsWith('-')) continue; // an option belonging to whatever command it turns out to be
|
|
200
|
+
positional.push(tok);
|
|
201
|
+
}
|
|
202
|
+
if (positional.length === 0) return true;
|
|
203
|
+
|
|
204
|
+
let cmd = program;
|
|
205
|
+
for (const tok of positional) {
|
|
206
|
+
if (cmd._actionHandler) break; // reached a leaf; remaining tokens are ITS args, not subcommands
|
|
207
|
+
if (tok === 'help') return true; // commander's help subcommand, only in subcommand position
|
|
208
|
+
const next = (cmd.commands || []).find((c) => c.name() === tok || (c.aliases && c.aliases().includes(tok)));
|
|
209
|
+
if (!next) return false; // unresolvable path — commander's own error path, not ours to fetch for
|
|
210
|
+
cmd = next;
|
|
211
|
+
}
|
|
212
|
+
return !cmd._actionHandler;
|
|
213
|
+
}
|
package/lib/commands/airdrop.js
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import fs from 'fs';
|
|
31
|
+
// "May I prompt?" has ONE OWNER. This site holds no TTY check of its own.
|
|
32
|
+
import { requireInteractive } from '../interactive.js';
|
|
31
33
|
import readline from 'readline';
|
|
32
34
|
import chalk from 'chalk';
|
|
33
35
|
import { DeSciXApiClient } from '../api-client.js';
|
|
@@ -65,6 +67,14 @@ export function readSignerPkFromFile(path) {
|
|
|
65
67
|
* @returns {Promise<string>} Validated PK in `0x` + 64 hex form.
|
|
66
68
|
*/
|
|
67
69
|
export async function promptSignerPkInteractive({ stdin = process.stdin, stdout = process.stdout } = {}) {
|
|
70
|
+
// Gate FIRST: a non-TTY here previously hung forever holding a secret prompt open. This site
|
|
71
|
+
// keeps its own readline because it must disable terminal echo so the PK is never displayed;
|
|
72
|
+
// the DECISION of whether prompting is allowed is still the owner's alone.
|
|
73
|
+
requireInteractive({
|
|
74
|
+
what: 'descix airdrop',
|
|
75
|
+
question: 'signer private key',
|
|
76
|
+
nonInteractiveForm: ['descix airdrop --signer-pk-file <path> # reads the key from a file, never echoed']
|
|
77
|
+
});
|
|
68
78
|
return await new Promise((resolve, reject) => {
|
|
69
79
|
const rl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
|
|
70
80
|
// Disable echo so the PK isn't visible while typing.
|
package/lib/commands/auth.js
CHANGED
|
@@ -264,7 +264,19 @@ export async function loginDevice(options = {}) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
const walletPath = WalletFileManager.getProjectWalletPath(workspaceRoot);
|
|
267
|
-
|
|
267
|
+
try {
|
|
268
|
+
await WalletFileManager.saveWalletFile(walletPath, walletData);
|
|
269
|
+
} catch (saveErr) {
|
|
270
|
+
spinner.fail(chalk.red('Login did not complete'));
|
|
271
|
+
// The browser ceremony succeeded, but the platform returned no usable CLI credential for this
|
|
272
|
+
// account (e.g. an empty wallet signature). Say so plainly: nothing was written, and this
|
|
273
|
+
// workspace is NOT logged in on this environment.
|
|
274
|
+
throw Object.assign(new Error(
|
|
275
|
+
`The platform completed the browser sign-in but returned an unusable CLI credential (${saveErr.message}). ` +
|
|
276
|
+
`Nothing was saved and you are NOT logged in on ${apiClient.baseUrl}. ` +
|
|
277
|
+
`This is a platform defect, not something you can fix locally; report it with this message.`
|
|
278
|
+
), { code: 'UNUSABLE_CLI_CREDENTIAL' });
|
|
279
|
+
}
|
|
268
280
|
|
|
269
281
|
spinner.succeed(chalk.green('Login successful!'));
|
|
270
282
|
if (walletData.oauth) {
|
|
@@ -319,7 +331,9 @@ export async function loginDevice(options = {}) {
|
|
|
319
331
|
spinner.fail(chalk.red('Login failed'));
|
|
320
332
|
console.error(chalk.red(error.message));
|
|
321
333
|
|
|
322
|
-
if (error.
|
|
334
|
+
if (error.code === 'UNUSABLE_CLI_CREDENTIAL') {
|
|
335
|
+
// The message above is the whole diagnosis; connectivity tips would misdirect.
|
|
336
|
+
} else if (error.message.includes('expired')) {
|
|
323
337
|
diag(chalk.yellow('💡 Tip: Device codes expire after 15 minutes.'));
|
|
324
338
|
diag(chalk.yellow(' Run the login command again to get a new code.\n'));
|
|
325
339
|
} else {
|
package/lib/commands/config.js
CHANGED
|
@@ -9,6 +9,8 @@ import chalk from 'chalk';
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
11
11
|
import { environmentNameFor } from '../environment-report.js';
|
|
12
|
+
import { resolveEffectiveOrigin } from '../api-client.js';
|
|
13
|
+
import { normalizeOrigin } from '../origin.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Show current configuration
|
|
@@ -20,15 +22,17 @@ export async function show() {
|
|
|
20
22
|
|
|
21
23
|
console.log(chalk.cyan('\n📋 DeSciX Workspace Configuration:\n'));
|
|
22
24
|
console.log(chalk.white(` Workspace: ${workspaceRoot}`));
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
console.log(chalk.white(` API URL: ${
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
// The origin every command in this shell will USE, and who chose it — not the file's value
|
|
26
|
+
// alone. An inherited DESCIX_API_URL outranks the workspace pin; showing only the pin made
|
|
27
|
+
// `config show` say dev while commands ran against production.
|
|
28
|
+
const effective = await resolveEffectiveOrigin(workspaceConfig.env?.apiUrl);
|
|
29
|
+
console.log(chalk.white(` API URL: ${effective.origin}`));
|
|
30
|
+
console.log(chalk.white(` Environment: ${environmentNameFor(effective.origin)}`));
|
|
31
|
+
console.log(chalk.white(` Chosen by: ${effective.source}`));
|
|
32
|
+
const pinned = workspaceConfig.env?.apiUrl;
|
|
33
|
+
if (pinned && normalizeOrigin(pinned) !== effective.origin) {
|
|
34
|
+
console.log(chalk.yellow(` ⚠ This workspace pins ${normalizeOrigin(pinned)} (${environmentNameFor(pinned)}), but ${effective.source} overrides it for this shell.`));
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
// Show mapped apps (v2.1 env.products)
|
|
34
38
|
const platform = workspaceConfig.env?.platform;
|
|
@@ -49,28 +53,6 @@ export async function show() {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
/**
|
|
53
|
-
* Set API URL
|
|
54
|
-
*/
|
|
55
|
-
export async function setUrl(url, options = {}) {
|
|
56
|
-
try {
|
|
57
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
58
|
-
const workspaceRoot = workspaceConfig.getWorkspaceRoot();
|
|
59
|
-
|
|
60
|
-
workspaceConfig.apiUrl = url;
|
|
61
|
-
workspaceConfig.environment = url.includes('localhost') ? 'development' : 'production';
|
|
62
|
-
const configPath = await workspaceConfig.save(workspaceRoot);
|
|
63
|
-
|
|
64
|
-
console.log(chalk.green('\n✅ Configuration updated!\n'));
|
|
65
|
-
console.log(chalk.white(` API URL: ${url}`));
|
|
66
|
-
console.log(chalk.gray(` Saved to: ${configPath}\n`));
|
|
67
|
-
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.error(chalk.red('Error updating config:', error.message));
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
56
|
/**
|
|
75
57
|
* Initialize config for environment
|
|
76
58
|
*/
|