@opendatalabs/connect 0.10.0 → 0.10.1-canary.5944de8
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/dist/cli/auth.d.ts +112 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +339 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +657 -140
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/queries.d.ts.map +1 -1
- package/dist/cli/queries.js +16 -9
- package/dist/cli/queries.js.map +1 -1
- package/dist/cli/render/connect-renderer.d.ts +5 -0
- package/dist/cli/render/connect-renderer.d.ts.map +1 -1
- package/dist/cli/render/connect-renderer.js +62 -58
- package/dist/cli/render/connect-renderer.js.map +1 -1
- package/dist/cli/render/flow-theme.d.ts +17 -0
- package/dist/cli/render/flow-theme.d.ts.map +1 -0
- package/dist/cli/render/flow-theme.js +26 -0
- package/dist/cli/render/flow-theme.js.map +1 -0
- package/dist/cli/render/index.d.ts +1 -0
- package/dist/cli/render/index.d.ts.map +1 -1
- package/dist/cli/render/index.js +1 -0
- package/dist/cli/render/index.js.map +1 -1
- package/dist/cli/render/theme.d.ts +1 -0
- package/dist/cli/render/theme.d.ts.map +1 -1
- package/dist/cli/render/theme.js +19 -3
- package/dist/cli/render/theme.js.map +1 -1
- package/dist/core/cli-types.d.ts +3 -0
- package/dist/core/cli-types.d.ts.map +1 -1
- package/dist/core/cli-types.js +1 -1
- package/dist/core/cli-types.js.map +1 -1
- package/dist/personal-server/client.d.ts +1 -1
- package/dist/personal-server/client.d.ts.map +1 -1
- package/dist/personal-server/client.js +31 -17
- package/dist/personal-server/client.js.map +1 -1
- package/dist/personal-server/index.d.ts +12 -2
- package/dist/personal-server/index.d.ts.map +1 -1
- package/dist/personal-server/index.js +52 -18
- package/dist/personal-server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -26,14 +26,15 @@ const vanaPromptTheme = {
|
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
|
-
import { createConnectRenderer, createHumanRenderer, formatDisplayPath, formatRelativeTime, } from "./render/index.js";
|
|
29
|
+
import { createConnectRenderer, createHumanRenderer, createLoginRenderer, formatDisplayPath, formatRelativeTime, } from "./render/index.js";
|
|
30
30
|
import { CliOutcomeStatus, migrateLegacyDataHome, getBrowserProfilesDir, getConnectorCacheDir, getLogsDir, getSessionsDir, getSourceResultPath, readCliState, readCliConfig, updateCliConfig, updateSourceState, } from "../core/index.js";
|
|
31
31
|
import { fetchConnectorToCache, listAvailableSources, readCachedConnectorMetadata, } from "../connectors/registry.js";
|
|
32
|
-
import { detectPersonalServerTarget, ingestResult, } from "../personal-server/index.js";
|
|
32
|
+
import { detectPersonalServerTarget, ingestResult, resolvePersonalServerAuthConfig, } from "../personal-server/index.js";
|
|
33
33
|
import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/index.js";
|
|
34
34
|
import { listAvailableSkills, installSkill, readInstalledSkills, } from "../skills/index.js";
|
|
35
35
|
import { queryStatus, querySources, queryDataList, queryDataShow, queryDoctor, } from "./queries.js";
|
|
36
36
|
import { checkForUpdate, readUpdateCheck, isNewerVersion, } from "./update-check.js";
|
|
37
|
+
import { loadCredentials, saveCredentials, clearCredentials, isExpired, formatAddress, formatExpiresIn, getAuthTarget, resolvePersonalServerUrl, runDeviceCodeFlow, runSelfHostedLoginFlow, } from "./auth.js";
|
|
37
38
|
function cleanDescription(desc) {
|
|
38
39
|
return desc
|
|
39
40
|
.replace(/ using Playwright browser automation\.?/i, ".")
|
|
@@ -73,6 +74,7 @@ export async function runCli(argv = process.argv) {
|
|
|
73
74
|
.showSuggestionAfterError(true)
|
|
74
75
|
.addHelpText("after", `
|
|
75
76
|
Quick start:
|
|
77
|
+
vana login Log in to your Vana account
|
|
76
78
|
vana connect Connect a source and collect data
|
|
77
79
|
vana sources Browse available sources
|
|
78
80
|
vana status Check system health
|
|
@@ -330,6 +332,19 @@ Examples:
|
|
|
330
332
|
.action(async (scope) => {
|
|
331
333
|
process.exitCode = await runServerData(scope, parsedOptions);
|
|
332
334
|
});
|
|
335
|
+
program
|
|
336
|
+
.command("login")
|
|
337
|
+
.description("Log in to your Vana account or a self-hosted Personal Server")
|
|
338
|
+
.option("-s, --server <url>", "Self-hosted Personal Server URL")
|
|
339
|
+
.action(async (loginOptions) => {
|
|
340
|
+
process.exitCode = await runLogin(parsedOptions, loginOptions.server);
|
|
341
|
+
});
|
|
342
|
+
program
|
|
343
|
+
.command("logout")
|
|
344
|
+
.description("Log out and remove saved credentials")
|
|
345
|
+
.action(async () => {
|
|
346
|
+
process.exitCode = await runLogout(parsedOptions);
|
|
347
|
+
});
|
|
333
348
|
program
|
|
334
349
|
.command("mcp")
|
|
335
350
|
.description("Start MCP server for agent integration")
|
|
@@ -834,6 +849,8 @@ async function runConnect(rawSource, options) {
|
|
|
834
849
|
const ingestCompleted = ingestEvents.some((ingestEvent) => ingestEvent.type === "ingest-complete");
|
|
835
850
|
const ingestPartial = ingestEvents.some((ingestEvent) => ingestEvent.type === "ingest-partial");
|
|
836
851
|
const ingestFailedEvent = ingestEvents.find((ingestEvent) => ingestEvent.type === "ingest-failed");
|
|
852
|
+
const ingestSkippedUnavailable = ingestEvents.some((ingestEvent) => ingestEvent.type === "ingest-skipped" &&
|
|
853
|
+
ingestEvent.reason === "personal_server_unavailable");
|
|
837
854
|
if (ingestCompleted) {
|
|
838
855
|
finalStatus = CliOutcomeStatus.CONNECTED_AND_INGESTED;
|
|
839
856
|
finalDataState = "ingested_personal_server";
|
|
@@ -848,6 +865,10 @@ async function runConnect(rawSource, options) {
|
|
|
848
865
|
ingestFailureMessage =
|
|
849
866
|
ingestFailedEvent.message ?? "Personal Server sync failed.";
|
|
850
867
|
}
|
|
868
|
+
else if (ingestSkippedUnavailable) {
|
|
869
|
+
finalStatus = CliOutcomeStatus.CONNECTED_LOCAL_ONLY;
|
|
870
|
+
finalDataState = "ingest_unavailable";
|
|
871
|
+
}
|
|
851
872
|
else {
|
|
852
873
|
finalStatus = CliOutcomeStatus.CONNECTED_LOCAL_ONLY;
|
|
853
874
|
finalDataState = "collected_local";
|
|
@@ -920,6 +941,9 @@ async function runConnect(rawSource, options) {
|
|
|
920
941
|
else if (finalStatus === CliOutcomeStatus.CONNECTED_AND_INGESTED) {
|
|
921
942
|
successSummary = `Collected your ${displayName} data and synced it to your Personal Server.`;
|
|
922
943
|
}
|
|
944
|
+
else if (finalDataState === "ingest_unavailable") {
|
|
945
|
+
successSummary = `Collected your ${displayName} data. Personal Server sync is pending.`;
|
|
946
|
+
}
|
|
923
947
|
else {
|
|
924
948
|
successSummary = `Collected your ${displayName} data and saved it locally.`;
|
|
925
949
|
}
|
|
@@ -932,6 +956,10 @@ async function runConnect(rawSource, options) {
|
|
|
932
956
|
if (failedCount > 0 && storedCount > 0) {
|
|
933
957
|
renderer?.detail(`Retry: vana server sync`);
|
|
934
958
|
}
|
|
959
|
+
else if (finalDataState === "ingest_unavailable") {
|
|
960
|
+
renderer?.detail(`Pending sync will retry during scheduled collection.`);
|
|
961
|
+
renderer?.detail(`Retry now: vana server sync`);
|
|
962
|
+
}
|
|
935
963
|
// Journey-aware next step
|
|
936
964
|
const state = await readCliState();
|
|
937
965
|
const connectedSourceCount = Object.values(state.sources ?? {}).filter((s) => hasCollectedData(s?.dataState)).length;
|
|
@@ -1151,10 +1179,19 @@ async function runStatus(options) {
|
|
|
1151
1179
|
}
|
|
1152
1180
|
}
|
|
1153
1181
|
if (options.json) {
|
|
1182
|
+
const jsonAuthCreds = loadCredentials();
|
|
1154
1183
|
const compactJson = {
|
|
1155
1184
|
runtime: status.runtime,
|
|
1156
1185
|
personalServer: status.personalServer,
|
|
1157
1186
|
personalServerUrl: status.personalServerUrl,
|
|
1187
|
+
pendingSyncCount: status.pendingSyncCount ?? 0,
|
|
1188
|
+
auth: jsonAuthCreds
|
|
1189
|
+
? {
|
|
1190
|
+
authenticated: !isExpired(jsonAuthCreds),
|
|
1191
|
+
address: jsonAuthCreds.account.address,
|
|
1192
|
+
expires_at: jsonAuthCreds.account.expires_at,
|
|
1193
|
+
}
|
|
1194
|
+
: { authenticated: false },
|
|
1158
1195
|
sources: {
|
|
1159
1196
|
connected: status.summary?.connectedCount ?? 0,
|
|
1160
1197
|
needsAttention: status.summary?.needsAttentionCount ?? 0,
|
|
@@ -1177,60 +1214,55 @@ async function runStatus(options) {
|
|
|
1177
1214
|
else {
|
|
1178
1215
|
emit.keyValue("Personal Server", "not connected", "warning");
|
|
1179
1216
|
}
|
|
1180
|
-
|
|
1181
|
-
const
|
|
1217
|
+
// Auth state
|
|
1218
|
+
const authCreds = loadCredentials();
|
|
1219
|
+
if (authCreds && !isExpired(authCreds)) {
|
|
1220
|
+
emit.keyValue("Account", formatAddress(authCreds.account.address), "success");
|
|
1221
|
+
emit.keyValue("Auth", `Authenticated (expires in ${formatExpiresIn(authCreds.account.expires_at)})`, "success");
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
emit.keyValue("Account", "Not logged in", "muted");
|
|
1225
|
+
emit.keyValue("Auth", "Run `vana login` to authenticate", "muted");
|
|
1226
|
+
}
|
|
1227
|
+
const trackedSources = status.sources.filter(shouldDisplaySourceInStatus);
|
|
1228
|
+
const attentionSources = trackedSources
|
|
1229
|
+
.filter(isSourceAttention)
|
|
1230
|
+
.sort(compareAttentionPriority);
|
|
1231
|
+
const healthySources = trackedSources
|
|
1232
|
+
.filter((source) => !isSourceAttention(source))
|
|
1233
|
+
.sort(compareSourceStatusOrder);
|
|
1182
1234
|
const sourceParts = [
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1235
|
+
healthySources.length > 0
|
|
1236
|
+
? `${healthySources.length} healthy`
|
|
1237
|
+
: trackedSources.length > 0
|
|
1238
|
+
? "none healthy"
|
|
1239
|
+
: "none connected",
|
|
1240
|
+
...(attentionSources.length > 0
|
|
1241
|
+
? [
|
|
1242
|
+
`${attentionSources.length} need${attentionSources.length === 1 ? "s" : ""} attention`,
|
|
1243
|
+
]
|
|
1186
1244
|
: []),
|
|
1187
1245
|
];
|
|
1188
|
-
emit.keyValue("Sources", sourceParts.join(", "),
|
|
1246
|
+
emit.keyValue("Sources", sourceParts.join(", "), attentionSources.length > 0 && healthySources.length > 0
|
|
1189
1247
|
? "warning"
|
|
1190
|
-
:
|
|
1248
|
+
: healthySources.length > 0
|
|
1191
1249
|
? "success"
|
|
1192
1250
|
: "muted");
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
stored.dataState === "ingest_failed" ||
|
|
1198
|
-
stored.connectionHealth));
|
|
1199
|
-
if (connectedSources.length > 0) {
|
|
1251
|
+
if ((status.pendingSyncCount ?? 0) > 0) {
|
|
1252
|
+
emit.keyValue("Pending sync", `${status.pendingSyncCount} dataset(s)`, "warning");
|
|
1253
|
+
}
|
|
1254
|
+
if (attentionSources.length > 0) {
|
|
1200
1255
|
emit.blank();
|
|
1201
|
-
|
|
1202
|
-
for (const
|
|
1203
|
-
|
|
1204
|
-
const displayName = displaySource(sourceId, sourceLabels);
|
|
1205
|
-
const sourceStatus = status.sources.find((s) => s.source === sourceId);
|
|
1206
|
-
const sourceOverdue = sourceStatus?.isOverdue ?? false;
|
|
1207
|
-
const healthTone = sourceOverdue ? "warning" : toneForHealth(health);
|
|
1208
|
-
const healthLabel = health === "needs_reauth" ? "needs login" : health;
|
|
1209
|
-
const staleTag = sourceOverdue
|
|
1210
|
-
? ` ${emit.badge("stale", "warning")}`
|
|
1211
|
-
: "";
|
|
1212
|
-
const collectedAgo = stored?.lastCollectedAt
|
|
1213
|
-
? `collected ${formatRelativeTime(stored.lastCollectedAt)}`
|
|
1214
|
-
: "";
|
|
1215
|
-
emit.keyValue(` ${displayName}`, `${healthLabel}${staleTag} ${collectedAgo}`, healthTone);
|
|
1216
|
-
if ((health === "needs_reauth" || health === "error") &&
|
|
1217
|
-
stored?.connectionHealthReason) {
|
|
1218
|
-
const msg = formatHealthMessage(stored.connectionHealthReason);
|
|
1219
|
-
const ago = stored.connectionHealthChangedAt
|
|
1220
|
-
? ` (${formatRelativeTime(stored.connectionHealthChangedAt)})`
|
|
1221
|
-
: "";
|
|
1222
|
-
if (msg) {
|
|
1223
|
-
emit.detail(` \u21b3 ${msg}${ago} Run \`vana connect ${sourceId}\``);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
if (health === "needs_reauth" && !needsReauthSource) {
|
|
1227
|
-
needsReauthSource = sourceId;
|
|
1228
|
-
}
|
|
1256
|
+
emit.section(formatCountLabel("Needs attention", attentionSources.length));
|
|
1257
|
+
for (const source of attentionSources) {
|
|
1258
|
+
emitHumanStatusSource(emit, source, sourceLabels);
|
|
1229
1259
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1260
|
+
}
|
|
1261
|
+
if (healthySources.length > 0) {
|
|
1262
|
+
emit.blank();
|
|
1263
|
+
emit.section(formatCountLabel("Healthy", healthySources.length));
|
|
1264
|
+
for (const source of healthySources) {
|
|
1265
|
+
emitHumanStatusSource(emit, source, sourceLabels);
|
|
1234
1266
|
}
|
|
1235
1267
|
}
|
|
1236
1268
|
if (nextSteps.length > 0) {
|
|
@@ -1245,6 +1277,88 @@ async function runStatus(options) {
|
|
|
1245
1277
|
}
|
|
1246
1278
|
return 0;
|
|
1247
1279
|
}
|
|
1280
|
+
function shouldDisplaySourceInStatus(source) {
|
|
1281
|
+
return (source.installed ||
|
|
1282
|
+
Boolean(source.lastRunOutcome) ||
|
|
1283
|
+
source.dataState !== "none" ||
|
|
1284
|
+
Boolean(source.connectionHealth));
|
|
1285
|
+
}
|
|
1286
|
+
function emitHumanStatusSource(emit, source, sourceLabels) {
|
|
1287
|
+
const displayName = displaySource(source.source, sourceLabels);
|
|
1288
|
+
const presentation = getHumanStatusPresentation(source);
|
|
1289
|
+
const staleTag = source.isOverdue ? ` ${emit.badge("stale", "warning")}` : "";
|
|
1290
|
+
const collectedAgo = source.lastCollectedAt
|
|
1291
|
+
? `collected ${formatRelativeTime(source.lastCollectedAt)}`
|
|
1292
|
+
: "";
|
|
1293
|
+
emit.keyValue(` ${displayName}`, `${presentation.label}${staleTag}${collectedAgo ? ` ${collectedAgo}` : ""}`, presentation.tone);
|
|
1294
|
+
const detail = formatHumanStatusDetail(source);
|
|
1295
|
+
if (detail) {
|
|
1296
|
+
emit.detail(` \u21b3 ${detail}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function getHumanStatusPresentation(source) {
|
|
1300
|
+
if (source.dataState === "ingest_failed") {
|
|
1301
|
+
return { label: "sync failed", tone: "warning" };
|
|
1302
|
+
}
|
|
1303
|
+
if (source.dataState === "ingest_unavailable") {
|
|
1304
|
+
return { label: "pending sync", tone: "warning" };
|
|
1305
|
+
}
|
|
1306
|
+
if (source.connectionHealth === "needs_reauth") {
|
|
1307
|
+
return { label: "needs login", tone: "warning" };
|
|
1308
|
+
}
|
|
1309
|
+
if (source.connectionHealth === "error") {
|
|
1310
|
+
return { label: "error", tone: "error" };
|
|
1311
|
+
}
|
|
1312
|
+
const presentation = getSourceStatusPresentation(source);
|
|
1313
|
+
if (presentation.label === "needs input") {
|
|
1314
|
+
return { label: "needs login", tone: presentation.tone };
|
|
1315
|
+
}
|
|
1316
|
+
return presentation;
|
|
1317
|
+
}
|
|
1318
|
+
function formatHumanStatusDetail(source) {
|
|
1319
|
+
if (source.dataState === "ingest_failed") {
|
|
1320
|
+
return formatSyncFailureSummary(source);
|
|
1321
|
+
}
|
|
1322
|
+
if (source.dataState === "ingest_unavailable") {
|
|
1323
|
+
return "Personal Server unavailable. Pending sync will retry during scheduled collection. Run `vana server sync`.";
|
|
1324
|
+
}
|
|
1325
|
+
if (source.connectionHealth === "needs_reauth" ||
|
|
1326
|
+
source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
1327
|
+
return `Authentication required. Run \`vana connect ${source.source}\`.`;
|
|
1328
|
+
}
|
|
1329
|
+
if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
1330
|
+
return `Manual auth step required. Run \`vana connect ${source.source}\`.`;
|
|
1331
|
+
}
|
|
1332
|
+
if (source.connectionHealth === "error") {
|
|
1333
|
+
const message = formatHealthMessage(source.connectionHealthReason);
|
|
1334
|
+
return `${message ?? "Collection failed."} Run \`vana connect ${source.source}\`.`;
|
|
1335
|
+
}
|
|
1336
|
+
if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
1337
|
+
return `${humanizeIssue(source.lastError ?? "Collection failed")}. Run \`vana connect ${source.source}\`.`;
|
|
1338
|
+
}
|
|
1339
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
1340
|
+
return "No connector available. Run `vana sources`.";
|
|
1341
|
+
}
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
function formatSyncFailureSummary(source) {
|
|
1345
|
+
const failedScopes = source.ingestScopes?.filter((scope) => scope.status === "failed") ?? [];
|
|
1346
|
+
if (failedScopes.length === 0) {
|
|
1347
|
+
return `Personal Server sync failed. Run \`vana connect ${source.source}\`.`;
|
|
1348
|
+
}
|
|
1349
|
+
const groupedFailures = new Map();
|
|
1350
|
+
for (const failedScope of failedScopes) {
|
|
1351
|
+
const summary = humanizeIssue(failedScope.error ?? "Sync failed");
|
|
1352
|
+
groupedFailures.set(summary, (groupedFailures.get(summary) ?? 0) + 1);
|
|
1353
|
+
}
|
|
1354
|
+
const entries = Array.from(groupedFailures.entries());
|
|
1355
|
+
if (entries.length === 1) {
|
|
1356
|
+
const [summary, count] = entries[0];
|
|
1357
|
+
return `${summary}${count > 1 ? ` for ${count} scopes` : ""}. Run \`vana connect ${source.source}\`.`;
|
|
1358
|
+
}
|
|
1359
|
+
const summaryParts = entries.map(([summary, count]) => count > 1 ? `${summary} (${count})` : summary);
|
|
1360
|
+
return `${failedScopes.length} scopes failed to sync: ${summaryParts.join("; ")}. Run \`vana connect ${source.source}\`.`;
|
|
1361
|
+
}
|
|
1248
1362
|
async function runDoctor(options) {
|
|
1249
1363
|
const payload = await queryDoctor();
|
|
1250
1364
|
if (options.json) {
|
|
@@ -1353,9 +1467,11 @@ async function runServerStatus(options) {
|
|
|
1353
1467
|
? "(auto-detected)"
|
|
1354
1468
|
: target.source === "config"
|
|
1355
1469
|
? "(saved)"
|
|
1356
|
-
: target.source === "
|
|
1357
|
-
? "(from
|
|
1358
|
-
:
|
|
1470
|
+
: target.source === "auth"
|
|
1471
|
+
? "(from vana login)"
|
|
1472
|
+
: target.source === "env"
|
|
1473
|
+
? "(from VANA_PERSONAL_SERVER_URL)"
|
|
1474
|
+
: `(${target.source ?? "unknown"})`;
|
|
1359
1475
|
emit.keyValue("URL", `${target.url} ${urlSuffix}`, "muted");
|
|
1360
1476
|
}
|
|
1361
1477
|
const stateLabel = target.state === "available" ? "healthy" : "Not connected";
|
|
@@ -1937,6 +2053,105 @@ async function runCollect(source, options) {
|
|
|
1937
2053
|
}
|
|
1938
2054
|
return runConnect(source, options);
|
|
1939
2055
|
}
|
|
2056
|
+
function isPendingSyncDataState(dataState) {
|
|
2057
|
+
return dataState === "collected_local" || dataState === "ingest_unavailable";
|
|
2058
|
+
}
|
|
2059
|
+
function shouldRetryPendingSource(stored, mode) {
|
|
2060
|
+
if (!stored?.lastResultPath) {
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
if (isPendingSyncDataState(stored.dataState)) {
|
|
2064
|
+
return true;
|
|
2065
|
+
}
|
|
2066
|
+
if (mode === "manual") {
|
|
2067
|
+
return (stored.dataState === "ingest_failed" ||
|
|
2068
|
+
stored.ingestScopes?.some((scope) => scope.status === "failed") === true);
|
|
2069
|
+
}
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
2072
|
+
function buildRetryOptions(stored, mode) {
|
|
2073
|
+
if (!stored) {
|
|
2074
|
+
return undefined;
|
|
2075
|
+
}
|
|
2076
|
+
if (mode !== "manual") {
|
|
2077
|
+
return undefined;
|
|
2078
|
+
}
|
|
2079
|
+
const failedScopes = stored.ingestScopes
|
|
2080
|
+
?.filter((scope) => scope.status === "failed")
|
|
2081
|
+
.map((scope) => scope.scope) ?? [];
|
|
2082
|
+
return failedScopes.length > 0 ? { scopes: failedScopes } : undefined;
|
|
2083
|
+
}
|
|
2084
|
+
function mergeIngestScopes(previous, current) {
|
|
2085
|
+
if (!previous && !current) {
|
|
2086
|
+
return undefined;
|
|
2087
|
+
}
|
|
2088
|
+
const merged = new Map();
|
|
2089
|
+
for (const scope of previous ?? []) {
|
|
2090
|
+
merged.set(scope.scope, { ...scope });
|
|
2091
|
+
}
|
|
2092
|
+
const now = new Date().toISOString();
|
|
2093
|
+
for (const scope of current ?? []) {
|
|
2094
|
+
merged.set(scope.scope, {
|
|
2095
|
+
scope: scope.scope,
|
|
2096
|
+
status: scope.status === "stored" ? "stored" : "failed",
|
|
2097
|
+
syncedAt: scope.status === "stored" ? now : merged.get(scope.scope)?.syncedAt,
|
|
2098
|
+
error: scope.status === "failed" ? scope.error : undefined,
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
return Array.from(merged.values()).sort((left, right) => left.scope.localeCompare(right.scope));
|
|
2102
|
+
}
|
|
2103
|
+
function deriveSyncedDataState(scopes, fallback) {
|
|
2104
|
+
if (!scopes || scopes.length === 0) {
|
|
2105
|
+
return fallback ?? "ingest_unavailable";
|
|
2106
|
+
}
|
|
2107
|
+
const storedCount = scopes.filter((scope) => scope.status === "stored").length;
|
|
2108
|
+
const failedCount = scopes.filter((scope) => scope.status === "failed").length;
|
|
2109
|
+
if (storedCount === 0 && failedCount > 0) {
|
|
2110
|
+
return "ingest_failed";
|
|
2111
|
+
}
|
|
2112
|
+
return "ingested_personal_server";
|
|
2113
|
+
}
|
|
2114
|
+
function summarizeSyncError(scopes) {
|
|
2115
|
+
const failedScopes = scopes?.filter((scope) => scope.status === "failed") ?? [];
|
|
2116
|
+
if (failedScopes.length === 0) {
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
return failedScopes
|
|
2120
|
+
.map((scope) => `${scope.scope}: ${scope.error ?? "sync failed"}`)
|
|
2121
|
+
.join("; ");
|
|
2122
|
+
}
|
|
2123
|
+
async function syncPendingSources(target, mode) {
|
|
2124
|
+
const state = await readCliState();
|
|
2125
|
+
const pendingSources = Object.entries(state.sources).filter(([, stored]) => shouldRetryPendingSource(stored, mode));
|
|
2126
|
+
if (pendingSources.length === 0) {
|
|
2127
|
+
return { syncedCount: 0, sourceResults: [] };
|
|
2128
|
+
}
|
|
2129
|
+
let syncedCount = 0;
|
|
2130
|
+
const sourceResults = [];
|
|
2131
|
+
for (const [source, stored] of pendingSources) {
|
|
2132
|
+
if (!stored?.lastResultPath) {
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
const ingestOptions = buildRetryOptions(stored, mode);
|
|
2136
|
+
const ingestEvents = await ingestResult(source, stored.lastResultPath, target, ingestOptions);
|
|
2137
|
+
const resultEvent = ingestEvents.find((event) => event.type === "ingest-complete" ||
|
|
2138
|
+
event.type === "ingest-partial" ||
|
|
2139
|
+
event.type === "ingest-failed");
|
|
2140
|
+
const mergedScopes = mergeIngestScopes(stored.ingestScopes, resultEvent?.scopeResults);
|
|
2141
|
+
const ingestCompleted = ingestEvents.some((event) => event.type === "ingest-complete");
|
|
2142
|
+
const ingestPartial = ingestEvents.some((event) => event.type === "ingest-partial");
|
|
2143
|
+
if (ingestCompleted || ingestPartial) {
|
|
2144
|
+
syncedCount++;
|
|
2145
|
+
await updateSourceState(source, {
|
|
2146
|
+
dataState: deriveSyncedDataState(mergedScopes, stored.dataState),
|
|
2147
|
+
ingestScopes: mergedScopes,
|
|
2148
|
+
lastError: summarizeSyncError(mergedScopes),
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2151
|
+
sourceResults.push({ source, scopeResults: resultEvent?.scopeResults });
|
|
2152
|
+
}
|
|
2153
|
+
return { syncedCount, sourceResults };
|
|
2154
|
+
}
|
|
1940
2155
|
async function runCollectAll(options) {
|
|
1941
2156
|
const emit = createEmitter(options);
|
|
1942
2157
|
const state = await readCliState();
|
|
@@ -1944,22 +2159,37 @@ async function runCollectAll(options) {
|
|
|
1944
2159
|
.filter(([, stored]) => stored?.connectorInstalled &&
|
|
1945
2160
|
isCollectionDue(stored.exportFrequency, stored.lastCollectedAt))
|
|
1946
2161
|
.map(([id]) => id);
|
|
2162
|
+
let exitCode = 0;
|
|
2163
|
+
for (const source of dueSources) {
|
|
2164
|
+
const result = await runConnect(source, options);
|
|
2165
|
+
if (result !== 0) {
|
|
2166
|
+
exitCode = result;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
let syncedPendingCount = 0;
|
|
2170
|
+
const target = await detectPersonalServerTarget();
|
|
2171
|
+
if (target.state === "available") {
|
|
2172
|
+
syncedPendingCount = (await syncPendingSources(target, "automatic"))
|
|
2173
|
+
.syncedCount;
|
|
2174
|
+
}
|
|
1947
2175
|
if (dueSources.length === 0) {
|
|
2176
|
+
if (syncedPendingCount > 0) {
|
|
2177
|
+
if (options.json) {
|
|
2178
|
+
process.stdout.write(`${JSON.stringify({ message: `Synced ${syncedPendingCount} pending dataset(s).`, count: 0, syncedPendingCount })}\n`);
|
|
2179
|
+
}
|
|
2180
|
+
else {
|
|
2181
|
+
emit.info(`Synced ${syncedPendingCount} pending dataset(s).`);
|
|
2182
|
+
}
|
|
2183
|
+
return 0;
|
|
2184
|
+
}
|
|
1948
2185
|
if (options.json) {
|
|
1949
|
-
process.stdout.write(`${JSON.stringify({ message: "No sources are due for collection.", count: 0 })}\n`);
|
|
2186
|
+
process.stdout.write(`${JSON.stringify({ message: "No sources are due for collection.", count: 0, syncedPendingCount: 0 })}\n`);
|
|
1950
2187
|
}
|
|
1951
2188
|
else {
|
|
1952
2189
|
emit.info("No sources are due for collection.");
|
|
1953
2190
|
}
|
|
1954
2191
|
return 0;
|
|
1955
2192
|
}
|
|
1956
|
-
let exitCode = 0;
|
|
1957
|
-
for (const source of dueSources) {
|
|
1958
|
-
const result = await runConnect(source, options);
|
|
1959
|
-
if (result !== 0) {
|
|
1960
|
-
exitCode = result;
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
2193
|
return exitCode;
|
|
1964
2194
|
}
|
|
1965
2195
|
async function runServerSync(options) {
|
|
@@ -1977,13 +2207,8 @@ async function runServerSync(options) {
|
|
|
1977
2207
|
}
|
|
1978
2208
|
return 1;
|
|
1979
2209
|
}
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
const pendingSources = Object.entries(state.sources).filter(([, stored]) => stored?.lastResultPath &&
|
|
1983
|
-
(stored.dataState === "collected_local" ||
|
|
1984
|
-
stored.dataState === "ingest_failed" ||
|
|
1985
|
-
stored?.ingestScopes?.some((s) => s.status === "failed")));
|
|
1986
|
-
if (pendingSources.length === 0) {
|
|
2210
|
+
const syncResult = await syncPendingSources(target, "manual");
|
|
2211
|
+
if (syncResult.sourceResults.length === 0) {
|
|
1987
2212
|
if (options.json) {
|
|
1988
2213
|
process.stdout.write(`${JSON.stringify({ message: "No pending datasets to sync.", syncedCount: 0 })}\n`);
|
|
1989
2214
|
}
|
|
@@ -1992,46 +2217,13 @@ async function runServerSync(options) {
|
|
|
1992
2217
|
}
|
|
1993
2218
|
return 0;
|
|
1994
2219
|
}
|
|
1995
|
-
let syncedCount = 0;
|
|
1996
|
-
const allScopeResults = [];
|
|
1997
|
-
for (const [source, stored] of pendingSources) {
|
|
1998
|
-
if (!stored?.lastResultPath) {
|
|
1999
|
-
continue;
|
|
2000
|
-
}
|
|
2001
|
-
const ingestEvents = await ingestResult(source, stored.lastResultPath, target);
|
|
2002
|
-
const resultEvent = ingestEvents.find((e) => e.type === "ingest-complete" ||
|
|
2003
|
-
e.type === "ingest-partial" ||
|
|
2004
|
-
e.type === "ingest-failed");
|
|
2005
|
-
const scopeResults = resultEvent?.scopeResults;
|
|
2006
|
-
const ingestCompleted = ingestEvents.some((e) => e.type === "ingest-complete");
|
|
2007
|
-
const ingestPartial = ingestEvents.some((e) => e.type === "ingest-partial");
|
|
2008
|
-
if (ingestCompleted || ingestPartial) {
|
|
2009
|
-
syncedCount++;
|
|
2010
|
-
const dataState = ingestCompleted || ingestPartial
|
|
2011
|
-
? "ingested_personal_server"
|
|
2012
|
-
: stored.dataState;
|
|
2013
|
-
await updateSourceState(source, {
|
|
2014
|
-
dataState,
|
|
2015
|
-
ingestScopes: scopeResults?.map((r) => ({
|
|
2016
|
-
scope: r.scope,
|
|
2017
|
-
status: r.status,
|
|
2018
|
-
syncedAt: r.status === "stored" ? new Date().toISOString() : undefined,
|
|
2019
|
-
error: r.error,
|
|
2020
|
-
})),
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
allScopeResults.push({ source, scopeResults });
|
|
2024
|
-
for (const event of ingestEvents) {
|
|
2025
|
-
emit.event(event);
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
2220
|
if (options.json) {
|
|
2029
|
-
process.stdout.write(`${JSON.stringify({ message: `Synced ${syncedCount} dataset(s).`, syncedCount })}\n`);
|
|
2221
|
+
process.stdout.write(`${JSON.stringify({ message: `Synced ${syncResult.syncedCount} dataset(s).`, syncedCount: syncResult.syncedCount })}\n`);
|
|
2030
2222
|
}
|
|
2031
2223
|
else {
|
|
2032
2224
|
// Show per-scope results with scope manifest style
|
|
2033
2225
|
const renderer = createHumanRenderer();
|
|
2034
|
-
for (const entry of
|
|
2226
|
+
for (const entry of syncResult.sourceResults) {
|
|
2035
2227
|
if (entry.scopeResults && entry.scopeResults.length > 0) {
|
|
2036
2228
|
emit.info(`${entry.source}:`);
|
|
2037
2229
|
for (const sr of entry.scopeResults) {
|
|
@@ -2046,9 +2238,9 @@ async function runServerSync(options) {
|
|
|
2046
2238
|
}
|
|
2047
2239
|
}
|
|
2048
2240
|
emit.blank();
|
|
2049
|
-
const allStored =
|
|
2241
|
+
const allStored = syncResult.sourceResults.every((entry) => !entry.scopeResults ||
|
|
2050
2242
|
entry.scopeResults.every((sr) => sr.status === "stored"));
|
|
2051
|
-
emit.success(`Synced ${syncedCount} dataset(s).`);
|
|
2243
|
+
emit.success(`Synced ${syncResult.syncedCount} dataset(s).`);
|
|
2052
2244
|
emit.blank();
|
|
2053
2245
|
if (allStored) {
|
|
2054
2246
|
emit.next("vana data list");
|
|
@@ -2074,12 +2266,20 @@ async function runServerData(scope, options) {
|
|
|
2074
2266
|
}
|
|
2075
2267
|
// If PS is available, try to list remote scopes via client
|
|
2076
2268
|
let remoteScopes = [];
|
|
2269
|
+
let didQueryRemoteScopes = false;
|
|
2077
2270
|
let remoteScopeFallbackReason;
|
|
2078
2271
|
if (target.state === "available" && target.url) {
|
|
2272
|
+
const auth = resolvePersonalServerAuthConfig(target.url);
|
|
2079
2273
|
try {
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2274
|
+
if (auth?.type === "bearerToken") {
|
|
2275
|
+
const { createPersonalServerClient: createClient } = await import("../personal-server/client.js");
|
|
2276
|
+
const client = createClient({
|
|
2277
|
+
url: target.url,
|
|
2278
|
+
auth,
|
|
2279
|
+
});
|
|
2280
|
+
didQueryRemoteScopes = true;
|
|
2281
|
+
remoteScopes = await client.listScopes(scope);
|
|
2282
|
+
}
|
|
2083
2283
|
}
|
|
2084
2284
|
catch (err) {
|
|
2085
2285
|
remoteScopeFallbackReason =
|
|
@@ -2087,7 +2287,7 @@ async function runServerData(scope, options) {
|
|
|
2087
2287
|
}
|
|
2088
2288
|
}
|
|
2089
2289
|
// Use remote scopes if available, otherwise fall back to local
|
|
2090
|
-
const scopeList =
|
|
2290
|
+
const scopeList = didQueryRemoteScopes
|
|
2091
2291
|
? remoteScopes.map((s) => ({
|
|
2092
2292
|
scope: s.scope,
|
|
2093
2293
|
detail: `${s.count} version${s.count !== 1 ? "s" : ""}`,
|
|
@@ -2100,13 +2300,15 @@ async function runServerData(scope, options) {
|
|
|
2100
2300
|
process.stdout.write(`${JSON.stringify({
|
|
2101
2301
|
count: scopeList.length,
|
|
2102
2302
|
scopes: scopeList,
|
|
2103
|
-
source:
|
|
2303
|
+
source: didQueryRemoteScopes ? "remote" : "local",
|
|
2104
2304
|
...(remoteScopeFallbackReason ? { remoteScopeFallbackReason } : {}),
|
|
2105
2305
|
})}\n`);
|
|
2106
2306
|
return 0;
|
|
2107
2307
|
}
|
|
2108
2308
|
if (scopeList.length === 0) {
|
|
2109
|
-
emit.info(
|
|
2309
|
+
emit.info(didQueryRemoteScopes
|
|
2310
|
+
? "No data on your Personal Server."
|
|
2311
|
+
: "No scopes found.");
|
|
2110
2312
|
if (target.state !== "available") {
|
|
2111
2313
|
emit.detail("Personal Server is not available. Showing locally-known scopes only.");
|
|
2112
2314
|
}
|
|
@@ -2115,7 +2317,7 @@ async function runServerData(scope, options) {
|
|
|
2115
2317
|
for (const entry of scopeList) {
|
|
2116
2318
|
emit.keyValue(entry.scope, entry.detail, "muted");
|
|
2117
2319
|
}
|
|
2118
|
-
if (
|
|
2320
|
+
if (!didQueryRemoteScopes && localScopes.length > 0) {
|
|
2119
2321
|
emit.blank();
|
|
2120
2322
|
emit.detail("Showing locally-known scopes. Connect your Personal Server for live data.");
|
|
2121
2323
|
}
|
|
@@ -2303,6 +2505,62 @@ export function humanizeIssue(message) {
|
|
|
2303
2505
|
if (/checksum|mismatch/i.test(message)) {
|
|
2304
2506
|
return "Connector is out of date. Will auto-update on next connect.";
|
|
2305
2507
|
}
|
|
2508
|
+
const transportSummary = summarizeTransportError(message);
|
|
2509
|
+
if (transportSummary) {
|
|
2510
|
+
return transportSummary;
|
|
2511
|
+
}
|
|
2512
|
+
return message;
|
|
2513
|
+
}
|
|
2514
|
+
function summarizeTransportError(message) {
|
|
2515
|
+
const httpMatch = message.match(/^HTTP\s+\d+:\s*(.+)$/s);
|
|
2516
|
+
if (!httpMatch) {
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
const payload = httpMatch[1].trim();
|
|
2520
|
+
const parsed = parseTransportErrorPayload(payload);
|
|
2521
|
+
if (parsed) {
|
|
2522
|
+
return parsed.message;
|
|
2523
|
+
}
|
|
2524
|
+
const messageMatch = payload.match(/"message"\s*:\s*"([^"]+)"/);
|
|
2525
|
+
if (messageMatch) {
|
|
2526
|
+
return messageMatch[1];
|
|
2527
|
+
}
|
|
2528
|
+
return "Request failed";
|
|
2529
|
+
}
|
|
2530
|
+
function parseTransportErrorPayload(payload) {
|
|
2531
|
+
try {
|
|
2532
|
+
const parsed = JSON.parse(payload);
|
|
2533
|
+
const rawError = typeof parsed.error === "string"
|
|
2534
|
+
? parsed.error
|
|
2535
|
+
: parsed.error && typeof parsed.error === "object"
|
|
2536
|
+
? (parsed.error.errorCode ??
|
|
2537
|
+
parsed.error.code)
|
|
2538
|
+
: undefined;
|
|
2539
|
+
const rawMessage = typeof parsed.message === "string"
|
|
2540
|
+
? parsed.message
|
|
2541
|
+
: parsed.error && typeof parsed.error === "object"
|
|
2542
|
+
? parsed.error.message
|
|
2543
|
+
: undefined;
|
|
2544
|
+
const code = typeof rawError === "string" || typeof rawError === "number"
|
|
2545
|
+
? String(rawError)
|
|
2546
|
+
: undefined;
|
|
2547
|
+
const message = typeof rawMessage === "string" ? rawMessage : "Request failed";
|
|
2548
|
+
return { code, message: summarizeTransportCode(code, message) };
|
|
2549
|
+
}
|
|
2550
|
+
catch {
|
|
2551
|
+
return null;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function summarizeTransportCode(code, message) {
|
|
2555
|
+
if (code === "MISSING_AUTH" || /missing authentication/i.test(message)) {
|
|
2556
|
+
return "Authentication required";
|
|
2557
|
+
}
|
|
2558
|
+
if (code === "NO_SCHEMA" || /no schema registered/i.test(message)) {
|
|
2559
|
+
return "No schema registered";
|
|
2560
|
+
}
|
|
2561
|
+
if (code === "INVALID_SCOPE" || /scope must be/i.test(message)) {
|
|
2562
|
+
return "Unsupported scope";
|
|
2563
|
+
}
|
|
2306
2564
|
return message;
|
|
2307
2565
|
}
|
|
2308
2566
|
function formatHumanSourceMessage(message, source, displayName) {
|
|
@@ -2336,11 +2594,13 @@ export async function gatherSourceStatuses(storedSources, metadata = {}) {
|
|
|
2336
2594
|
const details = metadata[source];
|
|
2337
2595
|
const dataState = stored.dataState === "ingested_personal_server"
|
|
2338
2596
|
? "ingested_personal_server"
|
|
2339
|
-
: stored.dataState === "
|
|
2340
|
-
? "
|
|
2341
|
-
: stored.dataState === "
|
|
2342
|
-
? "
|
|
2343
|
-
: "
|
|
2597
|
+
: stored.dataState === "ingest_unavailable"
|
|
2598
|
+
? "ingest_unavailable"
|
|
2599
|
+
: stored.dataState === "ingest_failed"
|
|
2600
|
+
? "ingest_failed"
|
|
2601
|
+
: stored.dataState === "collected_local"
|
|
2602
|
+
? "collected_local"
|
|
2603
|
+
: "none";
|
|
2344
2604
|
const ingestScopes = stored.ingestScopes;
|
|
2345
2605
|
const syncedScopeCount = ingestScopes?.filter((s) => s.status === "stored").length ?? 0;
|
|
2346
2606
|
const failedScopeCount = ingestScopes?.filter((s) => s.status === "failed").length ?? 0;
|
|
@@ -2499,6 +2759,14 @@ function formatSourceStatusDetails(source) {
|
|
|
2499
2759
|
tone: "success",
|
|
2500
2760
|
});
|
|
2501
2761
|
}
|
|
2762
|
+
else if (source.dataState === "ingest_unavailable") {
|
|
2763
|
+
details.push({
|
|
2764
|
+
kind: "row",
|
|
2765
|
+
label: "State",
|
|
2766
|
+
value: "Pending sync to Personal Server",
|
|
2767
|
+
tone: "warning",
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2502
2770
|
else if (source.dataState === "ingest_failed") {
|
|
2503
2771
|
details.push({
|
|
2504
2772
|
kind: "row",
|
|
@@ -2554,11 +2822,15 @@ function formatSourceStatusDetails(source) {
|
|
|
2554
2822
|
}
|
|
2555
2823
|
export function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhealthy", availableSources = []) {
|
|
2556
2824
|
const nextSteps = [];
|
|
2557
|
-
const
|
|
2825
|
+
const attentionSources = [...sources]
|
|
2826
|
+
.filter(isSourceAttention)
|
|
2827
|
+
.sort(compareAttentionPriority);
|
|
2828
|
+
const highestPriority = attentionSources[0] ?? [...sources].sort(compareSourceStatusOrder)[0];
|
|
2558
2829
|
const connectedSources = sources.filter((source) => source.dataState === "collected_local" ||
|
|
2559
2830
|
source.dataState === "ingested_personal_server" ||
|
|
2560
|
-
source.dataState === "ingest_failed"
|
|
2561
|
-
|
|
2831
|
+
source.dataState === "ingest_failed" ||
|
|
2832
|
+
source.dataState === "ingest_unavailable");
|
|
2833
|
+
const needsAttention = attentionSources.length > 0;
|
|
2562
2834
|
const highestPriorityLabel = highestPriority
|
|
2563
2835
|
? displaySource(highestPriority.source, sourceLabels)
|
|
2564
2836
|
: null;
|
|
@@ -2581,8 +2853,22 @@ export function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhe
|
|
|
2581
2853
|
nextSteps.push("Inspect install health with `vana doctor`.");
|
|
2582
2854
|
}
|
|
2583
2855
|
}
|
|
2584
|
-
else if (highestPriority.
|
|
2585
|
-
nextSteps.push(`
|
|
2856
|
+
else if (highestPriority.dataState === "ingest_failed") {
|
|
2857
|
+
nextSteps.push(`Reconnect ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
|
|
2858
|
+
}
|
|
2859
|
+
else if (highestPriority.dataState === "ingest_unavailable") {
|
|
2860
|
+
nextSteps.push("Retry pending Personal Server sync with `vana server sync`.");
|
|
2861
|
+
}
|
|
2862
|
+
else if (highestPriority.connectionHealth === "needs_reauth" ||
|
|
2863
|
+
highestPriority.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
2864
|
+
nextSteps.push(`Reconnect ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
|
|
2865
|
+
if (highestPriority.lastLogPath) {
|
|
2866
|
+
nextSteps.push(`Inspect the latest run log with \`vana logs ${highestPriority.source}\`.`);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
else if (highestPriority.connectionHealth === "error" ||
|
|
2870
|
+
highestPriority.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
2871
|
+
nextSteps.push(`Retry ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
|
|
2586
2872
|
if (highestPriority.lastLogPath) {
|
|
2587
2873
|
nextSteps.push(`Inspect the latest run log with \`vana logs ${highestPriority.source}\`.`);
|
|
2588
2874
|
}
|
|
@@ -2600,8 +2886,7 @@ export function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhe
|
|
|
2600
2886
|
}
|
|
2601
2887
|
}
|
|
2602
2888
|
else if (highestPriority.dataState === "collected_local" ||
|
|
2603
|
-
highestPriority.dataState === "ingested_personal_server"
|
|
2604
|
-
highestPriority.dataState === "ingest_failed") {
|
|
2889
|
+
highestPriority.dataState === "ingested_personal_server") {
|
|
2605
2890
|
if (connectedSources.length > 1) {
|
|
2606
2891
|
nextSteps.push("Review your collected data with `vana data list`.");
|
|
2607
2892
|
}
|
|
@@ -2872,6 +3157,11 @@ export function getSourceStatusPresentation(source) {
|
|
|
2872
3157
|
if (!source.installed && !source.lastRunOutcome) {
|
|
2873
3158
|
return { label: "not connected", tone: "muted" };
|
|
2874
3159
|
}
|
|
3160
|
+
// Check dataState before early-returning on missing lastRunOutcome —
|
|
3161
|
+
// ingest can fail even when lastRunOutcome is unset
|
|
3162
|
+
if (source.dataState === "ingest_failed") {
|
|
3163
|
+
return { label: "sync failed", tone: "error" };
|
|
3164
|
+
}
|
|
2875
3165
|
if (!source.lastRunOutcome) {
|
|
2876
3166
|
return { label: "installed", tone: "success" };
|
|
2877
3167
|
}
|
|
@@ -2901,12 +3191,12 @@ export function getSourceStatusPresentation(source) {
|
|
|
2901
3191
|
}
|
|
2902
3192
|
return { label: "synced", tone: "success" };
|
|
2903
3193
|
}
|
|
3194
|
+
if (source.dataState === "ingest_unavailable") {
|
|
3195
|
+
return { label: "pending sync", tone: "warning" };
|
|
3196
|
+
}
|
|
2904
3197
|
if (source.dataState === "collected_local") {
|
|
2905
3198
|
return { label: "local", tone: "muted" };
|
|
2906
3199
|
}
|
|
2907
|
-
if (source.dataState === "ingest_failed") {
|
|
2908
|
-
return { label: "sync failed", tone: "error" };
|
|
2909
|
-
}
|
|
2910
3200
|
return { label: "connected", tone: "success" };
|
|
2911
3201
|
}
|
|
2912
3202
|
export function toneForRuntime(runtime) {
|
|
@@ -2993,6 +3283,48 @@ export function compareSourceStatusOrder(left, right) {
|
|
|
2993
3283
|
authMode: right.authMode,
|
|
2994
3284
|
}));
|
|
2995
3285
|
}
|
|
3286
|
+
export function isSourceAttention(source) {
|
|
3287
|
+
if (source.dataState === "ingest_failed" ||
|
|
3288
|
+
source.dataState === "ingest_unavailable") {
|
|
3289
|
+
return true;
|
|
3290
|
+
}
|
|
3291
|
+
if (source.connectionHealth === "needs_reauth" ||
|
|
3292
|
+
source.connectionHealth === "error" ||
|
|
3293
|
+
source.connectionHealth === "stale") {
|
|
3294
|
+
return true;
|
|
3295
|
+
}
|
|
3296
|
+
return rankSourceStatus(source) <= 4;
|
|
3297
|
+
}
|
|
3298
|
+
function compareAttentionPriority(left, right) {
|
|
3299
|
+
const rank = (source) => {
|
|
3300
|
+
if (source.dataState === "ingest_failed") {
|
|
3301
|
+
return 0;
|
|
3302
|
+
}
|
|
3303
|
+
if (source.dataState === "ingest_unavailable") {
|
|
3304
|
+
return 1;
|
|
3305
|
+
}
|
|
3306
|
+
if (source.connectionHealth === "needs_reauth") {
|
|
3307
|
+
return 2;
|
|
3308
|
+
}
|
|
3309
|
+
if (source.connectionHealth === "error") {
|
|
3310
|
+
return 3;
|
|
3311
|
+
}
|
|
3312
|
+
if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
3313
|
+
return 4;
|
|
3314
|
+
}
|
|
3315
|
+
if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
3316
|
+
return 5;
|
|
3317
|
+
}
|
|
3318
|
+
if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
3319
|
+
return 6;
|
|
3320
|
+
}
|
|
3321
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
3322
|
+
return 7;
|
|
3323
|
+
}
|
|
3324
|
+
return 8;
|
|
3325
|
+
};
|
|
3326
|
+
return rank(left) - rank(right) || compareSourceStatusOrder(left, right);
|
|
3327
|
+
}
|
|
2996
3328
|
export function rankSourceStatus(source) {
|
|
2997
3329
|
if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
2998
3330
|
return 0;
|
|
@@ -3009,6 +3341,9 @@ export function rankSourceStatus(source) {
|
|
|
3009
3341
|
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
3010
3342
|
return 4;
|
|
3011
3343
|
}
|
|
3344
|
+
if (source.dataState === "ingest_failed") {
|
|
3345
|
+
return 2;
|
|
3346
|
+
}
|
|
3012
3347
|
if (source.dataState === "ingested_personal_server") {
|
|
3013
3348
|
return 5;
|
|
3014
3349
|
}
|
|
@@ -3129,6 +3464,7 @@ function compareLogRecordOrder(left, right) {
|
|
|
3129
3464
|
}
|
|
3130
3465
|
export function hasCollectedData(dataState) {
|
|
3131
3466
|
return (dataState === "collected_local" ||
|
|
3467
|
+
dataState === "ingest_unavailable" ||
|
|
3132
3468
|
dataState === "ingested_personal_server" ||
|
|
3133
3469
|
dataState === "ingest_failed");
|
|
3134
3470
|
}
|
|
@@ -3148,6 +3484,9 @@ function formatLogOutcomeLabel(lastRunOutcome, dataState) {
|
|
|
3148
3484
|
if (dataState === "ingested_personal_server") {
|
|
3149
3485
|
return "synced";
|
|
3150
3486
|
}
|
|
3487
|
+
if (dataState === "ingest_unavailable") {
|
|
3488
|
+
return "pending sync";
|
|
3489
|
+
}
|
|
3151
3490
|
if (dataState === "ingest_failed") {
|
|
3152
3491
|
return "sync failed";
|
|
3153
3492
|
}
|
|
@@ -3169,7 +3508,8 @@ function toneForLogOutcome(lastRunOutcome, dataState) {
|
|
|
3169
3508
|
if (lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE ||
|
|
3170
3509
|
lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH ||
|
|
3171
3510
|
lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT ||
|
|
3172
|
-
dataState === "ingest_failed"
|
|
3511
|
+
dataState === "ingest_failed" ||
|
|
3512
|
+
dataState === "ingest_unavailable") {
|
|
3173
3513
|
return "warning";
|
|
3174
3514
|
}
|
|
3175
3515
|
if (dataState === "ingested_personal_server") {
|
|
@@ -3180,20 +3520,6 @@ function toneForLogOutcome(lastRunOutcome, dataState) {
|
|
|
3180
3520
|
}
|
|
3181
3521
|
return "muted";
|
|
3182
3522
|
}
|
|
3183
|
-
function toneForHealth(health) {
|
|
3184
|
-
switch (health) {
|
|
3185
|
-
case "healthy":
|
|
3186
|
-
return "accent";
|
|
3187
|
-
case "needs_reauth":
|
|
3188
|
-
return "warning";
|
|
3189
|
-
case "error":
|
|
3190
|
-
return "error";
|
|
3191
|
-
case "stale":
|
|
3192
|
-
return "muted";
|
|
3193
|
-
default:
|
|
3194
|
-
return "muted";
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
3523
|
// ---------------------------------------------------------------------------
|
|
3198
3524
|
// Detach (background process)
|
|
3199
3525
|
// ---------------------------------------------------------------------------
|
|
@@ -3899,4 +4225,195 @@ async function runSkillShow(name, options) {
|
|
|
3899
4225
|
return 1;
|
|
3900
4226
|
}
|
|
3901
4227
|
}
|
|
4228
|
+
// ── Login / Logout ─────────────────────────────────────────────────────
|
|
4229
|
+
async function runLogin(options, serverUrl) {
|
|
4230
|
+
// Determine auth target: cloud (account.vana.org) or self-hosted (PS directly)
|
|
4231
|
+
const psUrl = serverUrl ?? resolvePersonalServerUrl() ?? null;
|
|
4232
|
+
const authTarget = getAuthTarget(psUrl);
|
|
4233
|
+
// If self-hosted, use /auth/device flow against the PS
|
|
4234
|
+
if (authTarget === "self-hosted" && psUrl) {
|
|
4235
|
+
const renderer = !options.json && !options.quiet ? createLoginRenderer() : null;
|
|
4236
|
+
renderer?.title(psUrl);
|
|
4237
|
+
try {
|
|
4238
|
+
const result = await runSelfHostedLoginFlow(psUrl, (url) => {
|
|
4239
|
+
renderer?.note("Open this URL in your browser:");
|
|
4240
|
+
renderer?.note(url);
|
|
4241
|
+
renderer?.scopeActive("Waiting for authorization");
|
|
4242
|
+
// Try to open browser — use spawn with args array to prevent shell injection
|
|
4243
|
+
// (a malicious self-hosted PS could return a URL with shell metacharacters)
|
|
4244
|
+
try {
|
|
4245
|
+
const { spawn } = require("node:child_process");
|
|
4246
|
+
const opener = process.platform === "darwin"
|
|
4247
|
+
? "open"
|
|
4248
|
+
: process.platform === "win32"
|
|
4249
|
+
? "start"
|
|
4250
|
+
: "xdg-open";
|
|
4251
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
4252
|
+
}
|
|
4253
|
+
catch {
|
|
4254
|
+
// Browser open failed — user will open manually
|
|
4255
|
+
}
|
|
4256
|
+
});
|
|
4257
|
+
await saveCredentials({
|
|
4258
|
+
account: {
|
|
4259
|
+
address: result.address,
|
|
4260
|
+
session_token: "",
|
|
4261
|
+
expires_at: result.expires_at,
|
|
4262
|
+
},
|
|
4263
|
+
personal_server: {
|
|
4264
|
+
url: psUrl,
|
|
4265
|
+
session_token: result.session_token,
|
|
4266
|
+
expires_at: result.expires_at,
|
|
4267
|
+
},
|
|
4268
|
+
});
|
|
4269
|
+
await updateCliConfig({ personalServerUrl: psUrl });
|
|
4270
|
+
renderer?.success(`Logged in to ${psUrl}`);
|
|
4271
|
+
renderer?.detail("Credentials saved to ~/.vana/auth.json");
|
|
4272
|
+
return 0;
|
|
4273
|
+
}
|
|
4274
|
+
catch (err) {
|
|
4275
|
+
renderer?.fail("Login failed");
|
|
4276
|
+
renderer?.detail(err instanceof Error ? err.message : String(err));
|
|
4277
|
+
renderer?.next(`vana login --server ${psUrl}`);
|
|
4278
|
+
return 1;
|
|
4279
|
+
}
|
|
4280
|
+
finally {
|
|
4281
|
+
renderer?.cleanup();
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
// Cloud flow (account.vana.org)
|
|
4285
|
+
// Check env var shortcut
|
|
4286
|
+
const envToken = process.env.VANA_SESSION_TOKEN;
|
|
4287
|
+
if (envToken) {
|
|
4288
|
+
const creds = loadCredentials();
|
|
4289
|
+
if (creds) {
|
|
4290
|
+
if (options.json) {
|
|
4291
|
+
process.stdout.write(`${JSON.stringify({ status: "authenticated", source: "env", address: creds.account.address })}\n`);
|
|
4292
|
+
}
|
|
4293
|
+
else {
|
|
4294
|
+
const emit = createEmitter(options);
|
|
4295
|
+
emit.success(`Already authenticated via VANA_SESSION_TOKEN env var`);
|
|
4296
|
+
}
|
|
4297
|
+
return 0;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
// Check if already logged in
|
|
4301
|
+
const existing = loadCredentials();
|
|
4302
|
+
if (existing && !isExpired(existing)) {
|
|
4303
|
+
if (options.json) {
|
|
4304
|
+
process.stdout.write(`${JSON.stringify({
|
|
4305
|
+
status: "authenticated",
|
|
4306
|
+
address: existing.account.address,
|
|
4307
|
+
personal_server: existing.personal_server?.url ?? null,
|
|
4308
|
+
expires_at: existing.account.expires_at,
|
|
4309
|
+
})}\n`);
|
|
4310
|
+
}
|
|
4311
|
+
else {
|
|
4312
|
+
const emit = createEmitter(options);
|
|
4313
|
+
emit.success(`Already logged in as ${formatAddress(existing.account.address)}`);
|
|
4314
|
+
if (existing.personal_server) {
|
|
4315
|
+
emit.keyValue("Personal Server", existing.personal_server.url, "success");
|
|
4316
|
+
}
|
|
4317
|
+
emit.info(` Auth expires in ${formatExpiresIn(existing.account.expires_at)}`);
|
|
4318
|
+
emit.blank();
|
|
4319
|
+
emit.info(" Run `vana logout` first to re-authenticate.");
|
|
4320
|
+
}
|
|
4321
|
+
return 0;
|
|
4322
|
+
}
|
|
4323
|
+
if (options.json) {
|
|
4324
|
+
// JSON mode: run flow and output result
|
|
4325
|
+
const creds = await runDeviceCodeFlow({
|
|
4326
|
+
onCode: (code, uri) => {
|
|
4327
|
+
process.stderr.write(JSON.stringify({ event: "device_code", code, uri }) + "\n");
|
|
4328
|
+
},
|
|
4329
|
+
onWaiting: () => { },
|
|
4330
|
+
onAuthorized: () => { },
|
|
4331
|
+
onExpired: () => {
|
|
4332
|
+
process.stdout.write(JSON.stringify({ status: "expired", error: "Device code expired" }) +
|
|
4333
|
+
"\n");
|
|
4334
|
+
},
|
|
4335
|
+
onError: (err) => {
|
|
4336
|
+
process.stdout.write(JSON.stringify({ status: "error", error: err.message }) + "\n");
|
|
4337
|
+
},
|
|
4338
|
+
});
|
|
4339
|
+
if (creds) {
|
|
4340
|
+
await saveCredentials(creds);
|
|
4341
|
+
if (creds.personal_server?.url) {
|
|
4342
|
+
await updateCliConfig({ personalServerUrl: creds.personal_server.url });
|
|
4343
|
+
}
|
|
4344
|
+
process.stdout.write(`${JSON.stringify({
|
|
4345
|
+
status: "authenticated",
|
|
4346
|
+
address: creds.account.address,
|
|
4347
|
+
personal_server: creds.personal_server?.url ?? null,
|
|
4348
|
+
expires_at: creds.account.expires_at,
|
|
4349
|
+
})}\n`);
|
|
4350
|
+
return 0;
|
|
4351
|
+
}
|
|
4352
|
+
return 1;
|
|
4353
|
+
}
|
|
4354
|
+
// Interactive mode
|
|
4355
|
+
const renderer = createLoginRenderer();
|
|
4356
|
+
renderer.title("Vana");
|
|
4357
|
+
const creds = await runDeviceCodeFlow({
|
|
4358
|
+
onCode: (code, uri) => {
|
|
4359
|
+
renderer.note("Open this URL in your browser:");
|
|
4360
|
+
renderer.note(uri);
|
|
4361
|
+
renderer.note(`Enter this code: ${code}`);
|
|
4362
|
+
},
|
|
4363
|
+
onWaiting: () => {
|
|
4364
|
+
renderer.scopeActive("Waiting for authorization");
|
|
4365
|
+
},
|
|
4366
|
+
onAuthorized: async (authedCreds) => {
|
|
4367
|
+
await saveCredentials(authedCreds);
|
|
4368
|
+
if (authedCreds.personal_server?.url) {
|
|
4369
|
+
await updateCliConfig({
|
|
4370
|
+
personalServerUrl: authedCreds.personal_server.url,
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
renderer.success(`Logged in as ${formatAddress(authedCreds.account.address)}`);
|
|
4374
|
+
if (authedCreds.personal_server) {
|
|
4375
|
+
renderer.detail(`Personal Server: ${authedCreds.personal_server.url}`);
|
|
4376
|
+
}
|
|
4377
|
+
renderer.detail("Credentials saved to ~/.vana/auth.json");
|
|
4378
|
+
},
|
|
4379
|
+
onExpired: () => {
|
|
4380
|
+
renderer.fail("Authorization expired");
|
|
4381
|
+
renderer.next("vana login");
|
|
4382
|
+
},
|
|
4383
|
+
onError: (err) => {
|
|
4384
|
+
renderer.fail("Login failed");
|
|
4385
|
+
renderer.detail(err.message);
|
|
4386
|
+
renderer.next("vana login");
|
|
4387
|
+
},
|
|
4388
|
+
});
|
|
4389
|
+
renderer.cleanup();
|
|
4390
|
+
return creds ? 0 : 1;
|
|
4391
|
+
}
|
|
4392
|
+
async function runLogout(options) {
|
|
4393
|
+
// Revoke the token server-side before clearing local credentials
|
|
4394
|
+
const creds = loadCredentials();
|
|
4395
|
+
if (creds?.personal_server?.url && creds.personal_server.session_token) {
|
|
4396
|
+
try {
|
|
4397
|
+
await fetch(`${creds.personal_server.url.replace(/\/$/, "")}/auth/device/token`, {
|
|
4398
|
+
method: "DELETE",
|
|
4399
|
+
headers: {
|
|
4400
|
+
Authorization: `Bearer ${creds.personal_server.session_token}`,
|
|
4401
|
+
},
|
|
4402
|
+
signal: AbortSignal.timeout(5000),
|
|
4403
|
+
});
|
|
4404
|
+
}
|
|
4405
|
+
catch {
|
|
4406
|
+
// Best-effort — server may be down, but we still clear local creds
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
await clearCredentials();
|
|
4410
|
+
if (options.json) {
|
|
4411
|
+
process.stdout.write(`${JSON.stringify({ status: "logged_out" })}\n`);
|
|
4412
|
+
}
|
|
4413
|
+
else {
|
|
4414
|
+
const emit = createEmitter(options);
|
|
4415
|
+
emit.success("Logged out. Credentials removed.");
|
|
4416
|
+
}
|
|
4417
|
+
return 0;
|
|
4418
|
+
}
|
|
3902
4419
|
//# sourceMappingURL=index.js.map
|