vana-cli 0.15.1 → 0.23.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/README.md +73 -15
- package/dist/cli/app/ask.d.ts +38 -0
- package/dist/cli/app/ask.d.ts.map +1 -0
- package/dist/cli/app/ask.js +110 -0
- package/dist/cli/app/ask.js.map +1 -0
- package/dist/cli/app/derivatives.d.ts +41 -0
- package/dist/cli/app/derivatives.d.ts.map +1 -0
- package/dist/cli/app/derivatives.js +227 -0
- package/dist/cli/app/derivatives.js.map +1 -0
- package/dist/cli/app/escrow.d.ts +44 -0
- package/dist/cli/app/escrow.d.ts.map +1 -0
- package/dist/cli/app/escrow.js +275 -0
- package/dist/cli/app/escrow.js.map +1 -0
- package/dist/cli/app/index.d.ts +11 -0
- package/dist/cli/app/index.d.ts.map +1 -0
- package/dist/cli/app/index.js +151 -0
- package/dist/cli/app/index.js.map +1 -0
- package/dist/cli/app/onchain.d.ts +14 -0
- package/dist/cli/app/onchain.d.ts.map +1 -0
- package/dist/cli/app/onchain.js +84 -0
- package/dist/cli/app/onchain.js.map +1 -0
- package/dist/cli/app/outcome.d.ts +45 -0
- package/dist/cli/app/outcome.d.ts.map +1 -0
- package/dist/cli/app/outcome.js +55 -0
- package/dist/cli/app/outcome.js.map +1 -0
- package/dist/cli/app/read.d.ts +43 -0
- package/dist/cli/app/read.d.ts.map +1 -0
- package/dist/cli/app/read.js +429 -0
- package/dist/cli/app/read.js.map +1 -0
- package/dist/cli/app/register.d.ts +34 -0
- package/dist/cli/app/register.d.ts.map +1 -0
- package/dist/cli/app/register.js +113 -0
- package/dist/cli/app/register.js.map +1 -0
- package/dist/cli/app/request.d.ts +50 -0
- package/dist/cli/app/request.d.ts.map +1 -0
- package/dist/cli/app/request.js +252 -0
- package/dist/cli/app/request.js.map +1 -0
- package/dist/cli/app/requests.d.ts +21 -0
- package/dist/cli/app/requests.d.ts.map +1 -0
- package/dist/cli/app/requests.js +163 -0
- package/dist/cli/app/requests.js.map +1 -0
- package/dist/cli/app/whoami.d.ts +13 -0
- package/dist/cli/app/whoami.d.ts.map +1 -0
- package/dist/cli/app/whoami.js +71 -0
- package/dist/cli/app/whoami.js.map +1 -0
- package/dist/cli/auth.d.ts +18 -0
- package/dist/cli/auth.d.ts.map +1 -1
- package/dist/cli/auth.js +82 -4
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +304 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-server.d.ts.map +1 -1
- package/dist/cli/mcp-server.js +32 -3
- package/dist/cli/mcp-server.js.map +1 -1
- package/dist/core/app-key.d.ts +50 -0
- package/dist/core/app-key.d.ts.map +1 -0
- package/dist/core/app-key.js +171 -0
- package/dist/core/app-key.js.map +1 -0
- package/dist/core/assets.d.ts +33 -0
- package/dist/core/assets.d.ts.map +1 -0
- package/dist/core/assets.js +87 -0
- package/dist/core/assets.js.map +1 -0
- package/dist/core/cli-types.d.ts +30 -0
- package/dist/core/cli-types.d.ts.map +1 -1
- package/dist/core/cli-types.js +16 -0
- package/dist/core/cli-types.js.map +1 -1
- package/dist/core/exit-codes.d.ts +48 -0
- package/dist/core/exit-codes.d.ts.map +1 -0
- package/dist/core/exit-codes.js +96 -0
- package/dist/core/exit-codes.js.map +1 -0
- package/dist/core/network.d.ts +52 -0
- package/dist/core/network.d.ts.map +1 -0
- package/dist/core/network.js +83 -0
- package/dist/core/network.js.map +1 -0
- package/dist/core/paths.d.ts +4 -2
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +8 -6
- package/dist/core/paths.js.map +1 -1
- package/dist/core/receipts.d.ts +37 -0
- package/dist/core/receipts.d.ts.map +1 -0
- package/dist/core/receipts.js +72 -0
- package/dist/core/receipts.js.map +1 -0
- package/dist/core/requests-store.d.ts +54 -0
- package/dist/core/requests-store.d.ts.map +1 -0
- package/dist/core/requests-store.js +83 -0
- package/dist/core/requests-store.js.map +1 -0
- package/dist/personal-server/registered.d.ts +25 -0
- package/dist/personal-server/registered.d.ts.map +1 -0
- package/dist/personal-server/registered.js +49 -0
- package/dist/personal-server/registered.js.map +1 -0
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -2,11 +2,12 @@ import fs from "node:fs";
|
|
|
2
2
|
import fsp from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { spawn, execSync } from "node:child_process";
|
|
6
7
|
import os from "node:os";
|
|
7
8
|
import { confirm, input, password } from "@inquirer/prompts";
|
|
8
9
|
import { searchSelect } from "./search-select.js";
|
|
9
|
-
import { Command, CommanderError } from "commander";
|
|
10
|
+
import { Command, CommanderError, Option } from "commander";
|
|
10
11
|
// Vana-branded theme for inquirer prompts — matches brand palette
|
|
11
12
|
const VANA_BLUE = "\x1b[38;2;65;65;252m";
|
|
12
13
|
const VANA_GREEN = "\x1b[38;2;0;213;11m";
|
|
@@ -28,14 +29,91 @@ const vanaPromptTheme = {
|
|
|
28
29
|
};
|
|
29
30
|
import { createConnectRenderer, createHumanRenderer, createLoginRenderer, formatDisplayPath, formatRelativeTime, } from "./render/index.js";
|
|
30
31
|
import { CliOutcomeStatus, migrateLegacyDataHome, getBrowserProfilesDir, getConnectorCacheDir, getLogsDir, getSessionsDir, getSourceResultPath, readCliState, readCliConfig, updateCliConfig, updateSourceState, } from "../core/index.js";
|
|
32
|
+
import { UnknownNetworkError, VANA_NETWORKS, isVanaNetworkName, resolveNetwork, } from "../core/network.js";
|
|
33
|
+
import { CliExitCode } from "../core/exit-codes.js";
|
|
34
|
+
import { registerAppCommands } from "./app/index.js";
|
|
35
|
+
import { lookupRegisteredServers } from "../personal-server/registered.js";
|
|
31
36
|
import { fetchConnectorToCache, listAvailableSources, readCachedConnectorMetadata, } from "../connectors/registry.js";
|
|
32
37
|
import { detectPersonalServerTarget, ingestResult, resolvePersonalServerAuthConfig, } from "../personal-server/index.js";
|
|
33
38
|
import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/index.js";
|
|
34
39
|
import { listAvailableSkills, installSkill, readInstalledSkills, } from "../skills/index.js";
|
|
35
40
|
import { queryStatus, querySources, queryDataList, queryDataShow, queryDoctor, } from "./queries.js";
|
|
36
41
|
import { checkForUpdate, readUpdateCheck, isNewerVersion, } from "./update-check.js";
|
|
37
|
-
import { loadCredentials, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
|
|
42
|
+
import { loadCredentials, readStoredAuthFile, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
|
|
38
43
|
import { createCliTelemetrySession, flushTelemetryOutbox, getActiveTelemetrySession, getTelemetryStatus, setActiveTelemetrySession, setTelemetryEnabled, trackActiveTelemetryEvent, } from "./telemetry.js";
|
|
44
|
+
/** Long flags shared by every command; declared once, applied to all leaves. */
|
|
45
|
+
const GLOBAL_FLAG_DEFS = [
|
|
46
|
+
["--json", "Output machine-readable JSON"],
|
|
47
|
+
["--no-input", "Never prompt; fail or print a URL instead"],
|
|
48
|
+
["--yes", "Assume yes for confirmation prompts"],
|
|
49
|
+
["--quiet", "Suppress non-essential output"],
|
|
50
|
+
["--ipc", "Emit structured IPC events (internal)"],
|
|
51
|
+
["--detach", "Run in the background where supported"],
|
|
52
|
+
];
|
|
53
|
+
/**
|
|
54
|
+
* Declare the shared global flags on every leaf command that does not
|
|
55
|
+
* already define them, so commander accepts them anywhere on the line and
|
|
56
|
+
* `--help` documents them. Idempotent per flag.
|
|
57
|
+
*/
|
|
58
|
+
function applyGlobalOptions(command) {
|
|
59
|
+
if (command.commands.length === 0) {
|
|
60
|
+
for (const [flags, description] of GLOBAL_FLAG_DEFS) {
|
|
61
|
+
const long = `--${flags.replace(/^--(no-)?/, "")}`;
|
|
62
|
+
const exists = command.options.some((option) => option.long === flags || option.long === long);
|
|
63
|
+
if (!exists) {
|
|
64
|
+
command.option(flags, description);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!command.options.some((option) => option.long === "--network")) {
|
|
68
|
+
command.addOption(new Option("--network <network>", "Vana network").choices([
|
|
69
|
+
...VANA_NETWORKS,
|
|
70
|
+
]));
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const child of command.commands) {
|
|
75
|
+
applyGlobalOptions(child);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Merge the options commander actually parsed (root -> action command, child
|
|
80
|
+
* wins) into the pre-parse seed, so action handlers see the authoritative
|
|
81
|
+
* values while pre-parse consumers (update-notifier suppression, telemetry
|
|
82
|
+
* context) keep working from the argv scan.
|
|
83
|
+
*/
|
|
84
|
+
function mergeParsedGlobalOptions(target, actionCommand) {
|
|
85
|
+
const chain = [];
|
|
86
|
+
for (let current = actionCommand; current; current = current.parent) {
|
|
87
|
+
chain.unshift(current);
|
|
88
|
+
}
|
|
89
|
+
// Only values commander actually parsed count; a leaf's implicit
|
|
90
|
+
// default (e.g. --no-input declaring input: true) must never overwrite a
|
|
91
|
+
// value the user set at another level of the command line.
|
|
92
|
+
const merged = {};
|
|
93
|
+
for (const command of chain) {
|
|
94
|
+
const opts = command.opts();
|
|
95
|
+
for (const key of Object.keys(opts)) {
|
|
96
|
+
if (command.getOptionValueSource(key) !== "default") {
|
|
97
|
+
merged[key] = opts[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (merged.json === true)
|
|
102
|
+
target.json = true;
|
|
103
|
+
if (merged.input === false)
|
|
104
|
+
target.noInput = true;
|
|
105
|
+
if (merged.yes === true)
|
|
106
|
+
target.yes = true;
|
|
107
|
+
if (merged.quiet === true)
|
|
108
|
+
target.quiet = true;
|
|
109
|
+
if (merged.ipc === true)
|
|
110
|
+
target.ipc = true;
|
|
111
|
+
if (merged.detach === true)
|
|
112
|
+
target.detach = true;
|
|
113
|
+
if (typeof merged.network === "string" && isVanaNetworkName(merged.network)) {
|
|
114
|
+
target.network = merged.network;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
39
117
|
function cleanDescription(desc) {
|
|
40
118
|
return desc
|
|
41
119
|
.replace(/ using Playwright browser automation\.?/i, ".")
|
|
@@ -43,8 +121,10 @@ function cleanDescription(desc) {
|
|
|
43
121
|
}
|
|
44
122
|
const require = createRequire(import.meta.url);
|
|
45
123
|
export async function runCli(argv = process.argv) {
|
|
46
|
-
// Migrate ~/.dataconnect → ~/.vana, symlink old path for DataConnect compat
|
|
47
|
-
|
|
124
|
+
// Migrate ~/.dataconnect → ~/.vana, symlink old path for DataConnect compat.
|
|
125
|
+
// Only an actual data migration announces itself; the compat symlink that
|
|
126
|
+
// every fresh install gets on its second run stays silent.
|
|
127
|
+
if (migrateLegacyDataHome() === "migrated") {
|
|
48
128
|
process.stderr.write("Moved your data to ~/.vana.\n\n");
|
|
49
129
|
}
|
|
50
130
|
const normalizedArgv = normalizeArgv(argv);
|
|
@@ -110,6 +190,16 @@ More:
|
|
|
110
190
|
vana setup Install or repair runtime
|
|
111
191
|
`);
|
|
112
192
|
program.exitOverride();
|
|
193
|
+
// Global flags: declared here for `vana --help`, mirrored onto every leaf
|
|
194
|
+
// command by applyGlobalOptions() so they are accepted anywhere on the
|
|
195
|
+
// line. Values are merged back into parsedOptions in the preAction hook.
|
|
196
|
+
for (const [flags, description] of GLOBAL_FLAG_DEFS) {
|
|
197
|
+
program.option(flags, description);
|
|
198
|
+
}
|
|
199
|
+
program.addOption(new Option("--network <network>", "Vana network for protocol operations (default: moksha)").choices([...VANA_NETWORKS]));
|
|
200
|
+
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
201
|
+
mergeParsedGlobalOptions(parsedOptions, actionCommand);
|
|
202
|
+
});
|
|
113
203
|
program
|
|
114
204
|
.command("version")
|
|
115
205
|
.description("Print CLI version")
|
|
@@ -383,12 +473,14 @@ Examples:
|
|
|
383
473
|
telemetry
|
|
384
474
|
.command("enable")
|
|
385
475
|
.description("Enable telemetry")
|
|
476
|
+
.option("--json", "Output machine-readable JSON")
|
|
386
477
|
.action(async () => {
|
|
387
478
|
process.exitCode = await runTelemetryEnable(parsedOptions);
|
|
388
479
|
});
|
|
389
480
|
telemetry
|
|
390
481
|
.command("disable")
|
|
391
482
|
.description("Disable telemetry")
|
|
483
|
+
.option("--json", "Output machine-readable JSON")
|
|
392
484
|
.action(async () => {
|
|
393
485
|
process.exitCode = await runTelemetryDisable(parsedOptions);
|
|
394
486
|
});
|
|
@@ -468,6 +560,8 @@ Examples:
|
|
|
468
560
|
.action(async () => {
|
|
469
561
|
process.exitCode = await runCommandWithTelemetry({ ...telemetryBaseContext, command: "schedule", subcommand: "remove" }, async () => runScheduleRemove(parsedOptions));
|
|
470
562
|
});
|
|
563
|
+
registerAppCommands(program, () => parsedOptions);
|
|
564
|
+
applyGlobalOptions(program);
|
|
471
565
|
try {
|
|
472
566
|
await program.parseAsync(normalizedArgv);
|
|
473
567
|
}
|
|
@@ -479,8 +573,13 @@ Examples:
|
|
|
479
573
|
process.exitCode = error.exitCode;
|
|
480
574
|
return Number(process.exitCode ?? 0);
|
|
481
575
|
}
|
|
482
|
-
// Commander already printed to stderr
|
|
483
|
-
|
|
576
|
+
// Commander already printed to stderr. Usage errors (unknown command
|
|
577
|
+
// or option, missing or invalid argument) exit 2 per the exit-code
|
|
578
|
+
// table in docs/CLI-EXIT-CODE-MATRIX.md; anything else keeps
|
|
579
|
+
// commander's own code.
|
|
580
|
+
process.exitCode = error.code.startsWith("commander.")
|
|
581
|
+
? CliExitCode.USAGE
|
|
582
|
+
: error.exitCode;
|
|
484
583
|
return Number(process.exitCode ?? 1);
|
|
485
584
|
}
|
|
486
585
|
throw error;
|
|
@@ -767,9 +866,7 @@ async function runConnect(rawSource, options) {
|
|
|
767
866
|
connectorInstalled: false,
|
|
768
867
|
lastRunAt: new Date().toISOString(),
|
|
769
868
|
lastRunOutcome: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
|
|
770
|
-
dataState: "none",
|
|
771
869
|
lastError: message,
|
|
772
|
-
lastResultPath: null,
|
|
773
870
|
lastLogPath: getErrorLogPath(retryError),
|
|
774
871
|
});
|
|
775
872
|
renderer?.fail(`${displayName} connector could not be verified.`);
|
|
@@ -790,9 +887,7 @@ async function runConnect(rawSource, options) {
|
|
|
790
887
|
connectorInstalled: false,
|
|
791
888
|
lastRunAt: new Date().toISOString(),
|
|
792
889
|
lastRunOutcome: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
|
|
793
|
-
dataState: "none",
|
|
794
890
|
lastError: message,
|
|
795
|
-
lastResultPath: null,
|
|
796
891
|
lastLogPath: getErrorLogPath(firstError),
|
|
797
892
|
});
|
|
798
893
|
renderer?.fail(`${displayName} is not available.`);
|
|
@@ -845,9 +940,7 @@ async function runConnect(rawSource, options) {
|
|
|
845
940
|
sessionPresent: fs.existsSync(profilePath),
|
|
846
941
|
lastRunAt: new Date().toISOString(),
|
|
847
942
|
lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
|
|
848
|
-
dataState: "none",
|
|
849
943
|
lastError: message,
|
|
850
|
-
lastResultPath: null,
|
|
851
944
|
lastLogPath: fetchLogPath ?? null,
|
|
852
945
|
});
|
|
853
946
|
renderer?.fail(`${displayName} requires a browser window, but no display is available.`);
|
|
@@ -1002,8 +1095,6 @@ async function runConnect(rawSource, options) {
|
|
|
1002
1095
|
lastRunAt: new Date().toISOString(),
|
|
1003
1096
|
lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
|
|
1004
1097
|
lastError: event.message ?? "Legacy authentication is required.",
|
|
1005
|
-
dataState: "none",
|
|
1006
|
-
lastResultPath: null,
|
|
1007
1098
|
lastLogPath: event.logPath,
|
|
1008
1099
|
connectionHealth: "needs_reauth",
|
|
1009
1100
|
connectionHealthChangedAt: new Date().toISOString(),
|
|
@@ -1118,9 +1209,7 @@ async function runConnect(rawSource, options) {
|
|
|
1118
1209
|
sessionPresent: fs.existsSync(profilePath),
|
|
1119
1210
|
lastRunAt: new Date().toISOString(),
|
|
1120
1211
|
lastRunOutcome: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
|
|
1121
|
-
dataState: "none",
|
|
1122
1212
|
lastError: "Connector run ended without a result.",
|
|
1123
|
-
lastResultPath: null,
|
|
1124
1213
|
lastLogPath: runLogPath ?? fetchLogPath ?? null,
|
|
1125
1214
|
});
|
|
1126
1215
|
renderer?.fail(`Problem connecting ${displayName}.`);
|
|
@@ -1171,6 +1260,9 @@ async function runConnect(rawSource, options) {
|
|
|
1171
1260
|
else if (finalDataState === "ingest_unavailable") {
|
|
1172
1261
|
successSummary = `Collected your ${displayName} data. Personal Server sync is pending.`;
|
|
1173
1262
|
}
|
|
1263
|
+
else if (finalDataState === "ingest_failed") {
|
|
1264
|
+
successSummary = `Collected your ${displayName} data, but Personal Server sync failed.`;
|
|
1265
|
+
}
|
|
1174
1266
|
else {
|
|
1175
1267
|
successSummary = `Collected your ${displayName} data and saved it locally.`;
|
|
1176
1268
|
}
|
|
@@ -1187,6 +1279,14 @@ async function runConnect(rawSource, options) {
|
|
|
1187
1279
|
renderer?.detail(`Pending sync will retry during scheduled collection.`);
|
|
1188
1280
|
renderer?.detail(`Retry now: vana server sync`);
|
|
1189
1281
|
}
|
|
1282
|
+
else if (finalDataState === "ingest_failed") {
|
|
1283
|
+
if (ingestFailureMessage?.includes("401")) {
|
|
1284
|
+
renderer?.detail("Your Personal Server requires authentication. Run `vana login` to authenticate, then `vana server sync`.");
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
renderer?.detail(`Retry: vana server sync`);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1190
1290
|
// Journey-aware next step
|
|
1191
1291
|
const state = await readCliState();
|
|
1192
1292
|
const connectedSourceCount = Object.values(state.sources ?? {}).filter((s) => hasCollectedData(s?.dataState)).length;
|
|
@@ -1288,15 +1388,15 @@ async function runConnectEntry(options) {
|
|
|
1288
1388
|
}
|
|
1289
1389
|
: null,
|
|
1290
1390
|
})}\n`);
|
|
1291
|
-
return
|
|
1391
|
+
return CliExitCode.USAGE;
|
|
1292
1392
|
}
|
|
1293
1393
|
if (options.noInput) {
|
|
1294
1394
|
emit.info(missingSourceMessage);
|
|
1295
|
-
return
|
|
1395
|
+
return CliExitCode.USAGE;
|
|
1296
1396
|
}
|
|
1297
1397
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1298
1398
|
emit.info(missingSourceMessage);
|
|
1299
|
-
return
|
|
1399
|
+
return CliExitCode.USAGE;
|
|
1300
1400
|
}
|
|
1301
1401
|
if (enrichedSources.length === 0) {
|
|
1302
1402
|
emit.info("No sources are available right now.");
|
|
@@ -1587,7 +1687,41 @@ function formatSyncFailureSummary(source) {
|
|
|
1587
1687
|
return `${failedScopes.length} scopes failed to sync: ${summaryParts.join("; ")}. Run \`vana connect ${source.source}\`.`;
|
|
1588
1688
|
}
|
|
1589
1689
|
async function runDoctor(options) {
|
|
1590
|
-
const
|
|
1690
|
+
const basePayload = await queryDoctor();
|
|
1691
|
+
// Builder-side protocol network (owner-side server config is separate and
|
|
1692
|
+
// reported by its own checks). Additive field; the rest of the payload
|
|
1693
|
+
// still matches CliDoctor exactly. A bad VANA_NETWORK/VANA_ENV combination
|
|
1694
|
+
// is a usage problem, not a crash: exit 2 with a structured message.
|
|
1695
|
+
let network;
|
|
1696
|
+
try {
|
|
1697
|
+
network = resolveNetwork(options.network);
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
if (error instanceof UnknownNetworkError) {
|
|
1701
|
+
if (options.json) {
|
|
1702
|
+
process.stdout.write(`${JSON.stringify({ error: "bad_usage", message: error.message })}\n`);
|
|
1703
|
+
}
|
|
1704
|
+
else {
|
|
1705
|
+
process.stderr.write(`${error.message}\n`);
|
|
1706
|
+
}
|
|
1707
|
+
return CliExitCode.USAGE;
|
|
1708
|
+
}
|
|
1709
|
+
throw error;
|
|
1710
|
+
}
|
|
1711
|
+
const payload = {
|
|
1712
|
+
...basePayload,
|
|
1713
|
+
network: {
|
|
1714
|
+
name: network.name,
|
|
1715
|
+
env: network.env,
|
|
1716
|
+
chainId: network.chainId,
|
|
1717
|
+
gatewayUrl: network.gatewayUrl,
|
|
1718
|
+
source: options.network
|
|
1719
|
+
? "flag"
|
|
1720
|
+
: process.env.VANA_NETWORK
|
|
1721
|
+
? "env"
|
|
1722
|
+
: "default",
|
|
1723
|
+
},
|
|
1724
|
+
};
|
|
1591
1725
|
if (options.json) {
|
|
1592
1726
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1593
1727
|
return 0;
|
|
@@ -1603,6 +1737,7 @@ async function runDoctor(options) {
|
|
|
1603
1737
|
emit.keyValue("Install", formatInstallMethodLabel(payload.installMethod), "muted");
|
|
1604
1738
|
emit.keyValue("Runtime", payload.runtime, toneForRuntime(payload.runtime));
|
|
1605
1739
|
emit.keyValue("Personal Server", payload.personalServer, payload.personalServer === "available" ? "success" : "warning");
|
|
1740
|
+
emit.keyValue("Network", `${payload.network.name} (chain ${payload.network.chainId}, ${payload.network.source})`, "muted");
|
|
1606
1741
|
emit.keyValue("Tracked sources", String(payload.summary.trackedSourceCount), "muted");
|
|
1607
1742
|
emit.keyValue("Attention", String(payload.summary.attentionCount), payload.summary.attentionCount > 0 ? "warning" : "muted");
|
|
1608
1743
|
emit.keyValue("Connected", String(payload.summary.connectedCount), payload.summary.connectedCount > 0 ? "success" : "muted");
|
|
@@ -1669,6 +1804,11 @@ async function runDoctor(options) {
|
|
|
1669
1804
|
async function runServerStatus(options) {
|
|
1670
1805
|
const emit = createEmitter(options);
|
|
1671
1806
|
const target = await detectPersonalServerTarget();
|
|
1807
|
+
// The local transport URL and the gateway-registered public URL are two
|
|
1808
|
+
// names for the same server; show both when the owner is known.
|
|
1809
|
+
const registeredServers = target.health?.owner
|
|
1810
|
+
? await lookupRegisteredServers(target.health.owner)
|
|
1811
|
+
: [];
|
|
1672
1812
|
const state = await readCliState();
|
|
1673
1813
|
// Count scopes from state
|
|
1674
1814
|
let totalScopeCount = 0;
|
|
@@ -1682,6 +1822,7 @@ async function runServerStatus(options) {
|
|
|
1682
1822
|
state: target.state,
|
|
1683
1823
|
url: target.url,
|
|
1684
1824
|
source: target.source,
|
|
1825
|
+
registeredServers,
|
|
1685
1826
|
health: target.health,
|
|
1686
1827
|
scopeCount: totalScopeCount,
|
|
1687
1828
|
})}\n`);
|
|
@@ -1701,6 +1842,9 @@ async function runServerStatus(options) {
|
|
|
1701
1842
|
: `(${target.source ?? "unknown"})`;
|
|
1702
1843
|
emit.keyValue("URL", `${target.url} ${urlSuffix}`, "muted");
|
|
1703
1844
|
}
|
|
1845
|
+
for (const server of registeredServers) {
|
|
1846
|
+
emit.keyValue("Registered", `${server.url} (${server.network})`, "muted");
|
|
1847
|
+
}
|
|
1704
1848
|
const stateLabel = target.state === "available" ? "healthy" : "Not connected";
|
|
1705
1849
|
emit.keyValue("Status", stateLabel, target.state === "available" ? "success" : "warning");
|
|
1706
1850
|
if (target.health) {
|
|
@@ -3293,8 +3437,25 @@ export function getCliChannel(version = getCliVersion()) {
|
|
|
3293
3437
|
? "canary"
|
|
3294
3438
|
: "stable";
|
|
3295
3439
|
}
|
|
3296
|
-
|
|
3297
|
-
|
|
3440
|
+
function normalizePathForMatch(value) {
|
|
3441
|
+
return value.replace(/\\/g, "/").toLowerCase();
|
|
3442
|
+
}
|
|
3443
|
+
/** Where this file itself sits, which is what separates npm from a checkout. */
|
|
3444
|
+
function getOwnModulePath() {
|
|
3445
|
+
try {
|
|
3446
|
+
return fileURLToPath(import.meta.url);
|
|
3447
|
+
}
|
|
3448
|
+
catch {
|
|
3449
|
+
return "";
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
/** True when the CLI is running out of an npm install or an npx cache. */
|
|
3453
|
+
function isPackagedUnderNodeModules(normalizedPath) {
|
|
3454
|
+
return (normalizedPath.includes("/node_modules/vana-cli/") ||
|
|
3455
|
+
normalizedPath.includes("/_npx/"));
|
|
3456
|
+
}
|
|
3457
|
+
export function getCliInstallMethod(execPath = process.execPath, modulePath = getOwnModulePath()) {
|
|
3458
|
+
const candidates = [process.env.VANA_APP_ROOT ?? "", execPath].map(normalizePathForMatch);
|
|
3298
3459
|
for (const normalizedPath of candidates) {
|
|
3299
3460
|
if (!normalizedPath) {
|
|
3300
3461
|
continue;
|
|
@@ -3308,6 +3469,18 @@ export function getCliInstallMethod(execPath = process.execPath) {
|
|
|
3308
3469
|
/\/releases\/[^/]+\/app$/.test(normalizedPath)) {
|
|
3309
3470
|
return "installer";
|
|
3310
3471
|
}
|
|
3472
|
+
}
|
|
3473
|
+
// npm and npx both run under the user's own node, so execPath looks exactly
|
|
3474
|
+
// like a development checkout. Only the package's own location tells them
|
|
3475
|
+
// apart, and getting it wrong costs an npm user every update notification.
|
|
3476
|
+
if (modulePath &&
|
|
3477
|
+
isPackagedUnderNodeModules(normalizePathForMatch(modulePath))) {
|
|
3478
|
+
return "npm";
|
|
3479
|
+
}
|
|
3480
|
+
for (const normalizedPath of candidates) {
|
|
3481
|
+
if (!normalizedPath) {
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3311
3484
|
if (normalizedPath.endsWith("/node") ||
|
|
3312
3485
|
normalizedPath.endsWith("/node.exe") ||
|
|
3313
3486
|
normalizedPath.includes("/.nvm/") ||
|
|
@@ -3336,14 +3509,28 @@ export function formatInstallMethodLabel(method) {
|
|
|
3336
3509
|
return "Homebrew";
|
|
3337
3510
|
case "installer":
|
|
3338
3511
|
return "Hosted installer";
|
|
3512
|
+
case "npm":
|
|
3513
|
+
return "npm";
|
|
3339
3514
|
case "development":
|
|
3340
3515
|
return "Development checkout";
|
|
3341
3516
|
default:
|
|
3342
3517
|
return "Unknown";
|
|
3343
3518
|
}
|
|
3344
3519
|
}
|
|
3345
|
-
export function getLifecycleCommands(installMethod, channel) {
|
|
3520
|
+
export function getLifecycleCommands(installMethod, channel, modulePath = getOwnModulePath()) {
|
|
3346
3521
|
switch (installMethod) {
|
|
3522
|
+
case "npm":
|
|
3523
|
+
// npx installs nothing, so there is nothing to upgrade or remove:
|
|
3524
|
+
// asking for @latest is the whole update story there.
|
|
3525
|
+
return normalizePathForMatch(modulePath).includes("/_npx/")
|
|
3526
|
+
? {
|
|
3527
|
+
upgrade: "npx vana-cli@latest",
|
|
3528
|
+
uninstall: "npx leaves nothing installed. Remove ~/.vana for any state you no longer need.",
|
|
3529
|
+
}
|
|
3530
|
+
: {
|
|
3531
|
+
upgrade: "npm install -g vana-cli@latest",
|
|
3532
|
+
uninstall: "npm uninstall -g vana-cli",
|
|
3533
|
+
};
|
|
3347
3534
|
case "homebrew":
|
|
3348
3535
|
return {
|
|
3349
3536
|
upgrade: "brew update && brew upgrade vana",
|
|
@@ -3374,7 +3561,30 @@ export function getLifecycleCommands(installMethod, channel) {
|
|
|
3374
3561
|
};
|
|
3375
3562
|
}
|
|
3376
3563
|
}
|
|
3377
|
-
|
|
3564
|
+
/**
|
|
3565
|
+
* Pre-parse seed for the global options. Needed before commander runs (the
|
|
3566
|
+
* update-notifier suppression and the telemetry context read it); the
|
|
3567
|
+
* preAction hook overlays the authoritative parsed values afterwards.
|
|
3568
|
+
*/
|
|
3569
|
+
function extractGlobalOptions(rawArgv) {
|
|
3570
|
+
// Everything after a bare "--" is positional; commander will not parse it
|
|
3571
|
+
// as options, so the seed must not either.
|
|
3572
|
+
const boundary = rawArgv.indexOf("--");
|
|
3573
|
+
const argv = boundary === -1 ? rawArgv : rawArgv.slice(0, boundary);
|
|
3574
|
+
let network;
|
|
3575
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
3576
|
+
const argument = argv[index];
|
|
3577
|
+
let candidate;
|
|
3578
|
+
if (argument === "--network") {
|
|
3579
|
+
candidate = argv[index + 1];
|
|
3580
|
+
}
|
|
3581
|
+
else if (argument.startsWith("--network=")) {
|
|
3582
|
+
candidate = argument.slice("--network=".length);
|
|
3583
|
+
}
|
|
3584
|
+
if (candidate && isVanaNetworkName(candidate)) {
|
|
3585
|
+
network = candidate;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3378
3588
|
return {
|
|
3379
3589
|
json: argv.includes("--json"),
|
|
3380
3590
|
noInput: argv.includes("--no-input"),
|
|
@@ -3382,6 +3592,7 @@ function extractGlobalOptions(argv) {
|
|
|
3382
3592
|
yes: argv.includes("--yes"),
|
|
3383
3593
|
quiet: argv.includes("--quiet"),
|
|
3384
3594
|
detach: argv.includes("--detach"),
|
|
3595
|
+
network,
|
|
3385
3596
|
};
|
|
3386
3597
|
}
|
|
3387
3598
|
export function createSourceLabelMap(sources) {
|
|
@@ -4337,7 +4548,7 @@ async function runSkillsGuidedPicker(options) {
|
|
|
4337
4548
|
// In JSON mode, fall back to list behavior
|
|
4338
4549
|
return runSkillList(options);
|
|
4339
4550
|
}
|
|
4340
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4551
|
+
if (options.noInput || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
4341
4552
|
return runSkillList(options);
|
|
4342
4553
|
}
|
|
4343
4554
|
try {
|
|
@@ -4506,23 +4717,32 @@ async function runLogin(options, serverUrl, clientId) {
|
|
|
4506
4717
|
try {
|
|
4507
4718
|
const result = await runSelfHostedLoginFlow(psUrl, (url) => {
|
|
4508
4719
|
if (!options.json && !options.quiet) {
|
|
4509
|
-
|
|
4510
|
-
|
|
4720
|
+
// Same emphasis rule as the cloud flow: the actionable URL
|
|
4721
|
+
// carries the Vana accent, the label stays muted.
|
|
4722
|
+
process.stderr.write(` ${humanRenderer.theme.muted("Open")} ${humanRenderer.theme.accent(url)}\n`);
|
|
4511
4723
|
}
|
|
4512
4724
|
renderer?.scopeActive("Waiting for authorization");
|
|
4513
4725
|
// Try to open browser — use spawn with args array to prevent shell injection
|
|
4514
|
-
// (a malicious self-hosted PS could return a URL with shell metacharacters)
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4726
|
+
// (a malicious self-hosted PS could return a URL with shell metacharacters).
|
|
4727
|
+
// Interactive terminals only: agents, CI and the test suite must never
|
|
4728
|
+
// pop a browser (a vitest run used to open the fixture URL for real).
|
|
4729
|
+
const browserAllowed = Boolean(process.stdout.isTTY) &&
|
|
4730
|
+
!options.json &&
|
|
4731
|
+
!options.noInput &&
|
|
4732
|
+
!process.env.VANA_NO_BROWSER;
|
|
4733
|
+
if (browserAllowed) {
|
|
4734
|
+
try {
|
|
4735
|
+
const { spawn } = require("node:child_process");
|
|
4736
|
+
const opener = process.platform === "darwin"
|
|
4737
|
+
? "open"
|
|
4738
|
+
: process.platform === "win32"
|
|
4739
|
+
? "start"
|
|
4740
|
+
: "xdg-open";
|
|
4741
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
4742
|
+
}
|
|
4743
|
+
catch {
|
|
4744
|
+
// Browser open failed — user will open manually
|
|
4745
|
+
}
|
|
4526
4746
|
}
|
|
4527
4747
|
});
|
|
4528
4748
|
await saveCredentials({
|
|
@@ -4570,6 +4790,10 @@ async function runLogin(options, serverUrl, clientId) {
|
|
|
4570
4790
|
}
|
|
4571
4791
|
// Check if already logged in
|
|
4572
4792
|
const existing = loadCredentials();
|
|
4793
|
+
// For the account-switch check below: the previous auth file content even
|
|
4794
|
+
// when the stored credentials have expired (loadCredentials hides those).
|
|
4795
|
+
const previousAuth = readStoredAuthFile();
|
|
4796
|
+
const previousAddress = previousAuth?.address ?? null;
|
|
4573
4797
|
if (existing && !isExpired(existing)) {
|
|
4574
4798
|
if (options.json) {
|
|
4575
4799
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -4610,13 +4834,27 @@ async function runLogin(options, serverUrl, clientId) {
|
|
|
4610
4834
|
},
|
|
4611
4835
|
}, { clientId });
|
|
4612
4836
|
if (creds) {
|
|
4837
|
+
if (!creds.personal_server &&
|
|
4838
|
+
previousAuth?.personalServer &&
|
|
4839
|
+
(!previousAddress || previousAddress === creds.account.address)) {
|
|
4840
|
+
// The cloud flow cannot mint PS sessions; keep the one we had.
|
|
4841
|
+
creds.personal_server = previousAuth.personalServer;
|
|
4842
|
+
}
|
|
4613
4843
|
await saveCredentials(creds);
|
|
4614
4844
|
// Always sync the pinned PS config to this login's result — including
|
|
4615
4845
|
// clearing it when this account has no PS, so a stale PS URL from a
|
|
4616
4846
|
// previously logged-in account can't linger and be used by mistake.
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4847
|
+
if (creds.personal_server?.url) {
|
|
4848
|
+
await updateCliConfig({
|
|
4849
|
+
personalServerUrl: creds.personal_server.url,
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
else if (previousAddress && previousAddress !== creds.account.address) {
|
|
4853
|
+
// A different account logged in: its pinned Personal Server URL is
|
|
4854
|
+
// stale. Same-account logins keep the pin - the prod token flow
|
|
4855
|
+
// carries no PS info, so its absence proves nothing.
|
|
4856
|
+
await updateCliConfig({ personalServerUrl: undefined });
|
|
4857
|
+
}
|
|
4620
4858
|
process.stdout.write(`${JSON.stringify({
|
|
4621
4859
|
status: "authenticated",
|
|
4622
4860
|
address: creds.account.address,
|
|
@@ -4630,34 +4868,46 @@ async function runLogin(options, serverUrl, clientId) {
|
|
|
4630
4868
|
// Interactive mode
|
|
4631
4869
|
const renderer = createLoginRenderer();
|
|
4632
4870
|
const humanRenderer = createHumanRenderer();
|
|
4633
|
-
|
|
4871
|
+
// Branding lives in emphasis, not chrome (CLI-BEAUTY doctrine: no
|
|
4872
|
+
// banners). The two things the user must act on - the URL and the code -
|
|
4873
|
+
// carry the Vana accent; everything around them stays muted.
|
|
4874
|
+
const writeDeviceCodePrompt = (code, uri) => {
|
|
4634
4875
|
if (options.json || options.quiet) {
|
|
4635
4876
|
return;
|
|
4636
4877
|
}
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
}
|
|
4878
|
+
const theme = humanRenderer.theme;
|
|
4879
|
+
process.stderr.write(` ${theme.muted("Open")} ${theme.accent(uri)}\n`);
|
|
4880
|
+
process.stderr.write(` ${theme.muted("Enter")} ${theme.heading(code)}\n`);
|
|
4640
4881
|
};
|
|
4641
4882
|
renderer.title("Vana");
|
|
4642
4883
|
const creds = await runDeviceCodeFlow({
|
|
4643
4884
|
onCode: (code, uri) => {
|
|
4644
|
-
|
|
4645
|
-
"Open this URL in your browser:",
|
|
4646
|
-
uri,
|
|
4647
|
-
`Enter this code: ${code}`,
|
|
4648
|
-
]);
|
|
4885
|
+
writeDeviceCodePrompt(code, uri);
|
|
4649
4886
|
},
|
|
4650
4887
|
onWaiting: () => {
|
|
4651
4888
|
renderer.scopeActive("Waiting for authorization");
|
|
4652
4889
|
},
|
|
4653
4890
|
onAuthorized: async (authedCreds) => {
|
|
4891
|
+
if (!authedCreds.personal_server &&
|
|
4892
|
+
previousAuth?.personalServer &&
|
|
4893
|
+
(!previousAddress || previousAddress === authedCreds.account.address)) {
|
|
4894
|
+
// The cloud flow cannot mint PS sessions; keep the one we had.
|
|
4895
|
+
authedCreds.personal_server = previousAuth.personalServer;
|
|
4896
|
+
}
|
|
4654
4897
|
await saveCredentials(authedCreds);
|
|
4655
4898
|
// Always sync the pinned PS config to this login's result — including
|
|
4656
4899
|
// clearing it when this account has no PS, so a stale PS URL from a
|
|
4657
4900
|
// previously logged-in account can't linger and be used by mistake.
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4901
|
+
if (authedCreds.personal_server?.url) {
|
|
4902
|
+
await updateCliConfig({
|
|
4903
|
+
personalServerUrl: authedCreds.personal_server.url,
|
|
4904
|
+
});
|
|
4905
|
+
}
|
|
4906
|
+
else if (previousAddress &&
|
|
4907
|
+
previousAddress !== authedCreds.account.address) {
|
|
4908
|
+
// See the JSON branch: clear only on an account switch.
|
|
4909
|
+
await updateCliConfig({ personalServerUrl: undefined });
|
|
4910
|
+
}
|
|
4661
4911
|
renderer.success(`Logged in as ${formatAddress(authedCreds.account.address)}`);
|
|
4662
4912
|
if (authedCreds.personal_server) {
|
|
4663
4913
|
renderer.detail(`Personal Server: ${authedCreds.personal_server.url}`);
|