@descix/cli 1.0.6 ā 1.0.7
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/agent-assets/guides/dynamic-site-pattern.md +10 -14
- 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/onboarding-flow.md +4 -4
- package/agent-assets/reference/app-creation.md +36 -101
- package/agent-assets/reference/architecture.md +24 -27
- package/agent-assets/reference/mcp-integration.md +14 -22
- package/agent-assets/reference/workspace-config.md +97 -340
- package/agent-assets/rules/descix_mcp.template.mdc +1 -1
- package/bin/descix.js +137 -75
- package/lib/api-client.js +30 -22
- package/lib/commands/airdrop.js +10 -0
- package/lib/commands/auth.js +16 -2
- package/lib/commands/briefer/index.js +8 -5
- package/lib/commands/config.js +13 -31
- package/lib/commands/corpus.js +161 -54
- package/lib/commands/health.js +116 -21
- package/lib/commands/init.js +17 -45
- package/lib/commands/kb.js +7 -0
- 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/wallet-file.js +36 -37
- package/lib/wizard/setup.js +104 -22
- package/lib/workspace-config.js +265 -163
- package/lib/workspace-identity.js +0 -1
- package/package.json +2 -2
package/bin/descix.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Command } from 'commander';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { DeSciXApiClient } from '../lib/api-client.js';
|
|
13
13
|
import { requireAuth } from '../lib/auth-guard.js';
|
|
14
|
-
import { WorkspaceConfig, unmappedAppMessage } from '../lib/workspace-config.js';
|
|
14
|
+
import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath, TOP_LEVEL_API_URL_REMEDY } from '../lib/workspace-config.js';
|
|
15
15
|
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
16
16
|
import { recordInvocationOrigin } from '../lib/origin.js';
|
|
17
17
|
// Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
|
|
@@ -29,6 +29,8 @@ import * as buyCommands from '../lib/commands/buy.js';
|
|
|
29
29
|
import * as creditsCommands from '../lib/commands/credits.js';
|
|
30
30
|
import * as airdropCommands from '../lib/commands/airdrop.js';
|
|
31
31
|
import { runInit } from '../lib/commands/init.js';
|
|
32
|
+
// "May I prompt?" has ONE OWNER. No command in this file derives it.
|
|
33
|
+
import { createPromptSession } from '../lib/interactive.js';
|
|
32
34
|
import * as updateCommands from '../lib/commands/update.js';
|
|
33
35
|
import { registerAllRetiredKbSync, refuseRetiredKbSync, CANONICAL_KB_SYNC } from '../lib/commands/retired-kb-sync.js';
|
|
34
36
|
import { runStatus } from '../lib/commands/status.js';
|
|
@@ -291,7 +293,9 @@ program
|
|
|
291
293
|
// --env=demo is supplied (because the subcommand owns its own flag space).
|
|
292
294
|
const parentEnv = program.opts().env || null;
|
|
293
295
|
const env = options.env || parentEnv || 'dev';
|
|
294
|
-
await runHealth({ ...options, env });
|
|
296
|
+
const result = await runHealth({ ...options, env });
|
|
297
|
+
// A health check that reports an unhealthy service and exits 0 is not a gate.
|
|
298
|
+
if (result && result.all_healthy === false) process.exitCode = 1;
|
|
295
299
|
} catch (error) {
|
|
296
300
|
console.error(chalk.red(`\nHealth check error: ${error.message}\n`));
|
|
297
301
|
process.exit(1);
|
|
@@ -612,11 +616,12 @@ communityCommand
|
|
|
612
616
|
console.log(chalk.white(` Network: Polygon (live)\n`));
|
|
613
617
|
|
|
614
618
|
if (!options.yes) {
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
+
const rl = createPromptSession({
|
|
620
|
+
what: 'descix community create',
|
|
621
|
+
destructive: true,
|
|
622
|
+
nonInteractiveForm: ['descix community create ... --yes # deploys a REAL token contract on live Polygon']
|
|
619
623
|
});
|
|
624
|
+
const answer = await rl.askRaw(chalk.yellow(` Create community "${options.name}" with token ${tokenSymbol} on live Polygon? [y/N] `));
|
|
620
625
|
rl.close();
|
|
621
626
|
if (answer.toLowerCase() !== 'y') {
|
|
622
627
|
console.log(chalk.gray('\n Aborted.\n'));
|
|
@@ -713,11 +718,15 @@ communityCommand
|
|
|
713
718
|
|
|
714
719
|
// Confirmation prompt (unless --yes)
|
|
715
720
|
if (!options.yes && hardDelete) {
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
721
|
+
const rl = createPromptSession({
|
|
722
|
+
what: 'descix community delete',
|
|
723
|
+
destructive: true,
|
|
724
|
+
nonInteractiveForm: [
|
|
725
|
+
'Re-run with --dry-run to see the plan without changing anything.',
|
|
726
|
+
'descix community delete ... --yes # PERMANENTLY deletes the community and every app in it'
|
|
727
|
+
]
|
|
720
728
|
});
|
|
729
|
+
const answer = await rl.askRaw(chalk.yellow(`\n Permanently delete community "${manifest.community_name}" and all ${manifest.app_count} app(s)? This cannot be undone. [y/N] `));
|
|
721
730
|
rl.close();
|
|
722
731
|
if (answer.toLowerCase() !== 'y') {
|
|
723
732
|
console.log(chalk.gray('\n Aborted.\n'));
|
|
@@ -803,12 +812,15 @@ communityCommand
|
|
|
803
812
|
}
|
|
804
813
|
|
|
805
814
|
if (!options.yes) {
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
815
|
+
const rl = createPromptSession({
|
|
816
|
+
what: 'descix community rename',
|
|
817
|
+
destructive: true,
|
|
818
|
+
nonInteractiveForm: [
|
|
819
|
+
'Re-run with --dry-run to see the plan without changing anything.',
|
|
820
|
+
'descix community rename ... --yes # re-tags vectors and DELETES the old community doc'
|
|
821
|
+
]
|
|
822
|
+
});
|
|
823
|
+
const answer = await rl.askRaw(chalk.yellow(`\n Execute rename ${oldId} -> ${newId}? Re-tags ${plan.pinecone.matched} vector(s) and deletes Community/${oldId} after verifying Community/${newId}. [y/N] `));
|
|
812
824
|
rl.close();
|
|
813
825
|
if (answer.toLowerCase() !== 'y') {
|
|
814
826
|
console.log(chalk.gray('\n Aborted.\n'));
|
|
@@ -1608,11 +1620,15 @@ appCommand
|
|
|
1608
1620
|
|
|
1609
1621
|
// Confirmation prompt (unless --yes)
|
|
1610
1622
|
if (!options.yes && hardDelete) {
|
|
1611
|
-
const
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1623
|
+
const rl = createPromptSession({
|
|
1624
|
+
what: 'descix app delete',
|
|
1625
|
+
destructive: true,
|
|
1626
|
+
nonInteractiveForm: [
|
|
1627
|
+
'Re-run with --dry-run to see the plan without changing anything.',
|
|
1628
|
+
`descix app delete -a ${appId} --yes # PERMANENTLY deletes the app (hard delete is the default; --soft only hides it)`
|
|
1629
|
+
]
|
|
1615
1630
|
});
|
|
1631
|
+
const answer = await rl.askRaw(chalk.yellow(`\n Permanently delete ${appId}? This cannot be undone. [y/N] `));
|
|
1616
1632
|
rl.close();
|
|
1617
1633
|
if (answer.toLowerCase() !== 'y') {
|
|
1618
1634
|
console.log(chalk.gray('\n Aborted.\n'));
|
|
@@ -1742,36 +1758,49 @@ appCommand
|
|
|
1742
1758
|
const newPath = options.path;
|
|
1743
1759
|
|
|
1744
1760
|
const workspaceConfig = await WorkspaceConfig.load();
|
|
1745
|
-
const
|
|
1746
|
-
if (!appConfig) {
|
|
1747
|
-
throw new Error(`App '${appId}' is not mapped. Run 'descix app init -a ${appId}' first.`);
|
|
1748
|
-
}
|
|
1761
|
+
const wsRoot = workspaceConfig.workspaceRoot || process.cwd();
|
|
1749
1762
|
|
|
1750
|
-
//
|
|
1763
|
+
// Read the RAW registry entry, never the RESOLVED one. getAppByAppId() resolves the stored
|
|
1764
|
+
// localPath, so on a workspace already carrying a rejected value it throws here ā before
|
|
1765
|
+
// any write is attempted. That is what made this verb refuse exactly when it was needed,
|
|
1766
|
+
// while the rejection told the user to fix it with the config verb and never by hand.
|
|
1767
|
+
// This command is about to REPLACE that value; it has no business resolving it.
|
|
1768
|
+
//
|
|
1769
|
+
// Display value only. setLocalPath() is the SINGLE authority on whether the app is mapped
|
|
1770
|
+
// and refuses if it is not ā deciding that here as well would be two derivations of one
|
|
1771
|
+
// fact, and they would not even agree: _buildAppIdMap (behind getAppEntry) requires BOTH
|
|
1772
|
+
// appId and localPath, while the live-entry walk matches on appId alone.
|
|
1773
|
+
const oldPath = workspaceConfig.getAppEntry(appId)?.localPath;
|
|
1774
|
+
|
|
1775
|
+
// VALIDATE AT THE WRITE, through the LOADER'S OWN resolver ā not a second copy of its rules.
|
|
1776
|
+
// If resolveWorkspacePath would reject this value on the next read, it is rejected now, and
|
|
1777
|
+
// nothing reaches disk. Writing a value the loader will refuse is what bricks a workspace.
|
|
1778
|
+
const absPath = resolveWorkspacePath(wsRoot, newPath, appId);
|
|
1779
|
+
|
|
1780
|
+
// Hard-fail if path doesn't exist or is not a directory ā checked at the path the LOADER
|
|
1781
|
+
// will resolve to, not at a CWD-relative path that only exists from where you happened to
|
|
1782
|
+
// be standing when you typed it.
|
|
1751
1783
|
let stat;
|
|
1752
1784
|
try {
|
|
1753
|
-
stat = await fs.stat(
|
|
1785
|
+
stat = await fs.stat(absPath);
|
|
1754
1786
|
} catch {
|
|
1755
|
-
throw new Error(
|
|
1787
|
+
throw new Error(
|
|
1788
|
+
`Path does not exist: ${absPath}\n` +
|
|
1789
|
+
` (localPath '${newPath}' resolved against workspace root ${wsRoot})`
|
|
1790
|
+
);
|
|
1756
1791
|
}
|
|
1757
1792
|
if (!stat.isDirectory()) {
|
|
1758
|
-
throw new Error(
|
|
1793
|
+
throw new Error(
|
|
1794
|
+
`Path is not a directory: ${absPath}\n` +
|
|
1795
|
+
` (localPath '${newPath}' resolved against workspace root ${wsRoot})`
|
|
1796
|
+
);
|
|
1759
1797
|
}
|
|
1760
1798
|
|
|
1761
|
-
//
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
// Update env.products entry
|
|
1767
|
-
const products = workspaceConfig.env?.products || [];
|
|
1768
|
-
for (const product of products) {
|
|
1769
|
-
if (product.appId === appId || product.app_id === appId) {
|
|
1770
|
-
product.localPath = newPath;
|
|
1771
|
-
break;
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
await workspaceConfig.save(wsRoot);
|
|
1799
|
+
// Write through the canonical setter, which covers env.platform AND env.products[]. The
|
|
1800
|
+
// loop this replaces walked env.products ONLY: on the platform app it matched nothing,
|
|
1801
|
+
// wrote nothing, and still printed success ā a loud failure turned into a silent lie at
|
|
1802
|
+
// the one entry that matters most.
|
|
1803
|
+
await workspaceConfig.setLocalPath(appId, newPath);
|
|
1775
1804
|
|
|
1776
1805
|
console.log(chalk.green(`\nā ${appId} local path updated`));
|
|
1777
1806
|
console.log(chalk.gray(` ${oldPath} ā ${newPath}\n`));
|
|
@@ -3049,15 +3078,13 @@ siteCommand
|
|
|
3049
3078
|
}
|
|
3050
3079
|
|
|
3051
3080
|
if (!options.confirm) {
|
|
3052
|
-
const
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
});
|
|
3057
|
-
|
|
3058
|
-
const answer = await new Promise(resolve => {
|
|
3059
|
-
rl.question(chalk.yellow(`\nā ļø Delete site for ${communityId}/${appId}${options.preview ? ' (preview)' : ''}? (y/N) `), resolve);
|
|
3081
|
+
const rl = createPromptSession({
|
|
3082
|
+
what: 'descix site delete',
|
|
3083
|
+
destructive: true,
|
|
3084
|
+
nonInteractiveForm: [`descix site delete -c ${communityId} -a ${appId} --confirm # deletes the uploaded site files`]
|
|
3060
3085
|
});
|
|
3086
|
+
|
|
3087
|
+
const answer = await rl.askRaw(chalk.yellow(`\nā ļø Delete site for ${communityId}/${appId}${options.preview ? ' (preview)' : ''}? (y/N) `));
|
|
3061
3088
|
rl.close();
|
|
3062
3089
|
|
|
3063
3090
|
if (answer.toLowerCase() !== 'y') {
|
|
@@ -4342,17 +4369,22 @@ configCommand
|
|
|
4342
4369
|
}
|
|
4343
4370
|
});
|
|
4344
4371
|
|
|
4372
|
+
// `config set-url` is RETIRED, and it is retired LOUDLY rather than deleted outright: a deleted
|
|
4373
|
+
// subcommand only earns commander's "unknown command", which names no replacement. It used to
|
|
4374
|
+
// assign a top-level `apiUrl` that save() never serialized, then print a success banner over a
|
|
4375
|
+
// value that landed nowhere ā the file was re-stamped and the origin never moved. Hidden from
|
|
4376
|
+
// --help; any invocation exits non-zero naming the three surfaces that actually set the origin.
|
|
4345
4377
|
configCommand
|
|
4346
|
-
.command('set-url')
|
|
4347
|
-
.description('
|
|
4348
|
-
.argument('
|
|
4349
|
-
.
|
|
4350
|
-
.action(
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4378
|
+
.command('set-url', { hidden: true })
|
|
4379
|
+
.description('retired ā see error text')
|
|
4380
|
+
.argument('[url]')
|
|
4381
|
+
.allowUnknownOption()
|
|
4382
|
+
.action(() => {
|
|
4383
|
+
fail(new Error(
|
|
4384
|
+
'"descix config set-url" is retired: it never wrote the origin it reported (a retired top-level\n' +
|
|
4385
|
+
'key that nothing reads). Set the API origin where it is read, env.apiUrl, with one of:\n' +
|
|
4386
|
+
` ${TOP_LEVEL_API_URL_REMEDY}`
|
|
4387
|
+
));
|
|
4356
4388
|
});
|
|
4357
4389
|
|
|
4358
4390
|
configCommand
|
|
@@ -4825,8 +4857,12 @@ program
|
|
|
4825
4857
|
const { WorkspaceConfig } = await import('../lib/workspace-config.js');
|
|
4826
4858
|
wsConfig = await WorkspaceConfig.load(process.cwd());
|
|
4827
4859
|
} catch (e) {
|
|
4828
|
-
|
|
4829
|
-
|
|
4860
|
+
// RELAY the loader's own diagnosis; do not re-derive a remedy here. This site used to
|
|
4861
|
+
// print "Run 'descix init' first" for EVERY failure, including a corrupt-but-present
|
|
4862
|
+
// workspace.json ā prescribing an overwrite of the file it had just failed to read,
|
|
4863
|
+
// and discarding the loader's message that said exactly what was wrong.
|
|
4864
|
+
console.error(chalk.red(e.message));
|
|
4865
|
+
console.error(chalk.gray(` Or use --scope entitlements, which needs no workspace.`));
|
|
4830
4866
|
process.exit(1);
|
|
4831
4867
|
}
|
|
4832
4868
|
const appIds = new Set();
|
|
@@ -5019,29 +5055,55 @@ program
|
|
|
5019
5055
|
await authCommands.loginDevice(loginOptions);
|
|
5020
5056
|
}
|
|
5021
5057
|
|
|
5022
|
-
// Step 2: Workspace init ā create workspace.json
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5058
|
+
// Step 2: Workspace init ā create workspace.json only when there is none ANYWHERE UP THE TREE.
|
|
5059
|
+
//
|
|
5060
|
+
// WHERE THE WORKSPACE ROOT IS has ONE OWNER: WorkspaceConfig.findWorkspaceRoot, which walks UP
|
|
5061
|
+
// exactly as load() does. This step used to derive it a THIRD time with fs.access on the target
|
|
5062
|
+
// path only (the wizard guard was the second). Run from a SUBDIRECTORY of an existing
|
|
5063
|
+
// workspace, that check saw nothing, runInit created a NESTED workspace.json whose
|
|
5064
|
+
// workspaceRoot pointed at the subdirectory, and it SHADOWED the parent for every later
|
|
5065
|
+
// resolution ā silently, with "Quickstart complete!" and exit 0.
|
|
5066
|
+
//
|
|
5067
|
+
// quickstart is an ONBOARDING flow: a user who asked to be set up and already IS set up has
|
|
5068
|
+
// SUCCEEDED, so this SKIPS and REPORTS and continues. `descix mcp quickstart` refuses non-zero
|
|
5069
|
+
// on the same fact because it was asked specifically to CREATE a workspace and cannot honour
|
|
5070
|
+
// that. Different verbs, different contracts, ONE OWNER for the underlying fact.
|
|
5071
|
+
const existingRoot = await WorkspaceConfig.findWorkspaceRoot(workspaceRoot);
|
|
5072
|
+
if (existingRoot) {
|
|
5028
5073
|
console.log(chalk.green('ā Workspace already initialized'));
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5074
|
+
if (path.resolve(existingRoot) !== path.resolve(workspaceRoot)) {
|
|
5075
|
+
console.log(chalk.gray(` Workspace root: ${existingRoot}`));
|
|
5076
|
+
console.log(chalk.gray(` You are in a subdirectory of it. Not creating a second workspace`));
|
|
5077
|
+
console.log(chalk.gray(` here ā a nested one would shadow the root for every command run`));
|
|
5078
|
+
console.log(chalk.gray(` from this directory.`));
|
|
5079
|
+
}
|
|
5080
|
+
} else {
|
|
5032
5081
|
console.log(chalk.cyan('\nš Initialize Workspace\n'));
|
|
5033
|
-
|
|
5082
|
+
// runInit's signature is (apiClient, options). Passing a single object put it in the
|
|
5083
|
+
// apiClient slot and left options defaulting to {}, so `path` was silently discarded and
|
|
5084
|
+
// runInit fell back to process.cwd(). It was MASKED here only because workspaceRoot IS
|
|
5085
|
+
// process.cwd() in this action ā the argument was in the wrong slot regardless, and would
|
|
5086
|
+
// have written the workspace to the wrong directory the moment that stopped being true.
|
|
5087
|
+
// This action has no apiClient of its own; null is passed explicitly rather than implied.
|
|
5088
|
+
await runInit(null, { path: workspaceRoot });
|
|
5034
5089
|
}
|
|
5035
5090
|
|
|
5091
|
+
// EVERY REMAINING STEP TARGETS THE RESOLVED ROOT, not the directory the command was typed in.
|
|
5092
|
+
// They read the workspace to learn the app, community and origin they must state, so pointing
|
|
5093
|
+
// them at a subdirectory that deliberately has no workspace.json makes them hard-fail on the
|
|
5094
|
+
// absence this command just chose to preserve. Resolving the root once and ferrying it is the
|
|
5095
|
+
// same one-owner rule that fixed the check above.
|
|
5096
|
+
const targetRoot = existingRoot || workspaceRoot;
|
|
5097
|
+
|
|
5036
5098
|
// Step 3: Generate agent instruction files
|
|
5037
5099
|
console.log(chalk.cyan('\nš Generating Agent Instructions\n'));
|
|
5038
|
-
const written = await generateAgentFiles(
|
|
5100
|
+
const written = await generateAgentFiles(targetRoot);
|
|
5039
5101
|
for (const f of written) {
|
|
5040
5102
|
console.log(chalk.green(` ā ${f}`));
|
|
5041
5103
|
}
|
|
5042
5104
|
|
|
5043
5105
|
// Step 4: Generate .vscode/mcp.json (skipped if DeSciX extension handles MCP)
|
|
5044
|
-
const mcpWritten = await generateMcpConfig(
|
|
5106
|
+
const mcpWritten = await generateMcpConfig(targetRoot);
|
|
5045
5107
|
if (mcpWritten) {
|
|
5046
5108
|
console.log(chalk.green(' ā .vscode/mcp.json'));
|
|
5047
5109
|
} else {
|
|
@@ -5051,7 +5113,7 @@ program
|
|
|
5051
5113
|
// Step 5: Copy SDK assets
|
|
5052
5114
|
try {
|
|
5053
5115
|
const { pullSdkAssets } = await import('../lib/wizard/setup.js');
|
|
5054
|
-
const pulled = await pullSdkAssets(
|
|
5116
|
+
const pulled = await pullSdkAssets(targetRoot);
|
|
5055
5117
|
if (pulled) console.log(chalk.green(' ā .descix/sdk-assets/'));
|
|
5056
5118
|
} catch { /* setup.js pullSdkAssets may not be exported ā skip */ }
|
|
5057
5119
|
|
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
|
/**
|
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 {
|
|
@@ -218,16 +218,19 @@ export async function runBriefer(options = {}) {
|
|
|
218
218
|
let workspaceConfig = null;
|
|
219
219
|
try {
|
|
220
220
|
workspaceConfig = await WorkspaceConfig.tryLoad(cwd);
|
|
221
|
-
} catch {
|
|
222
|
-
// tryLoad returns null on
|
|
223
|
-
//
|
|
224
|
-
//
|
|
221
|
+
} catch (error) {
|
|
222
|
+
// tryLoad returns null on a MISSING workspace; it throws only when workspace.json EXISTS
|
|
223
|
+
// and cannot be read. Hard-fail per philosophy ā do NOT proceed with a guessed default.
|
|
224
|
+
//
|
|
225
|
+
// The recovery text is the loader's own, not a second opinion invented here: it used to say
|
|
226
|
+
// "run `descix init` to regenerate", which prescribes overwriting the very file that could
|
|
227
|
+
// not be read, and which also contradicts the standing rule against hand-editing it.
|
|
225
228
|
throw new BrieferExtractorError({
|
|
226
229
|
code: BRIEFER_ERROR_CODES.PARSE_FAIL,
|
|
227
230
|
section: 'briefer entry',
|
|
228
231
|
source: '.descix/workspace.json',
|
|
229
232
|
expected: 'valid v2.1 workspace JSON',
|
|
230
|
-
recovery:
|
|
233
|
+
recovery: error.message
|
|
231
234
|
});
|
|
232
235
|
}
|
|
233
236
|
|
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
|
*/
|