@opendatalabs/connect 0.10.1 → 0.11.1
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 +2 -2
- 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 +648 -191
- 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,115 +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 dataState = stored?.dataState;
|
|
1208
|
-
// Show worst of connectionHealth and dataState
|
|
1209
|
-
let healthLabel;
|
|
1210
|
-
let healthTone;
|
|
1211
|
-
if (dataState === "ingest_failed") {
|
|
1212
|
-
healthLabel = "sync failed";
|
|
1213
|
-
healthTone = "warning";
|
|
1214
|
-
}
|
|
1215
|
-
else {
|
|
1216
|
-
healthTone = sourceOverdue ? "warning" : toneForHealth(health);
|
|
1217
|
-
healthLabel = health === "needs_reauth" ? "needs login" : health;
|
|
1218
|
-
}
|
|
1219
|
-
const staleTag = sourceOverdue
|
|
1220
|
-
? ` ${emit.badge("stale", "warning")}`
|
|
1221
|
-
: "";
|
|
1222
|
-
const collectedAgo = stored?.lastCollectedAt
|
|
1223
|
-
? `collected ${formatRelativeTime(stored.lastCollectedAt)}`
|
|
1224
|
-
: "";
|
|
1225
|
-
emit.keyValue(` ${displayName}`, `${healthLabel}${staleTag} ${collectedAgo}`, healthTone);
|
|
1226
|
-
if (dataState === "ingest_failed") {
|
|
1227
|
-
const errMsg = stored?.lastError ?? "sync failed";
|
|
1228
|
-
emit.detail(` \u21b3 ${errMsg}. Run \`vana connect ${sourceId}\``);
|
|
1229
|
-
}
|
|
1230
|
-
else if ((health === "needs_reauth" || health === "error") &&
|
|
1231
|
-
stored?.connectionHealthReason) {
|
|
1232
|
-
const msg = formatHealthMessage(stored.connectionHealthReason);
|
|
1233
|
-
const ago = stored.connectionHealthChangedAt
|
|
1234
|
-
? ` (${formatRelativeTime(stored.connectionHealthChangedAt)})`
|
|
1235
|
-
: "";
|
|
1236
|
-
if (msg) {
|
|
1237
|
-
emit.detail(` \u21b3 ${msg}${ago} Run \`vana connect ${sourceId}\``);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
if (health === "needs_reauth" && !needsReauthSource) {
|
|
1241
|
-
needsReauthSource = sourceId;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
if (needsReauthSource) {
|
|
1245
|
-
emit.blank();
|
|
1246
|
-
emit.next(`vana connect ${needsReauthSource}`);
|
|
1247
|
-
return 0;
|
|
1256
|
+
emit.section(formatCountLabel("Needs attention", attentionSources.length));
|
|
1257
|
+
for (const source of attentionSources) {
|
|
1258
|
+
emitHumanStatusSource(emit, source, sourceLabels);
|
|
1248
1259
|
}
|
|
1249
1260
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
emit.blank();
|
|
1256
|
-
}
|
|
1257
|
-
for (const source of hiddenAttentionSources) {
|
|
1258
|
-
const displayName = displaySource(source.source, sourceLabels);
|
|
1259
|
-
const presentation = getSourceStatusPresentation(source);
|
|
1260
|
-
const collectedAgo = source.lastCollectedAt
|
|
1261
|
-
? `collected ${formatRelativeTime(source.lastCollectedAt)}`
|
|
1262
|
-
: "";
|
|
1263
|
-
emit.keyValue(` ${displayName}`, `${presentation.label}${collectedAgo ? ` ${collectedAgo}` : ""}`, presentation.tone);
|
|
1264
|
-
// Show actionable detail line
|
|
1265
|
-
if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED &&
|
|
1266
|
-
source.ingestScopes) {
|
|
1267
|
-
const failedScopes = source.ingestScopes.filter((s) => s.status === "failed");
|
|
1268
|
-
for (const scope of failedScopes) {
|
|
1269
|
-
const errMsg = scope.error ?? "sync failed";
|
|
1270
|
-
emit.detail(` \u21b3 ${source.source}.${scope.scope}: ${errMsg}. Run \`vana connect ${source.source}\``);
|
|
1271
|
-
}
|
|
1272
|
-
if (failedScopes.length === 0) {
|
|
1273
|
-
emit.detail(` \u21b3 Sync failed. Run \`vana connect ${source.source}\``);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
else if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
1277
|
-
emit.detail(` \u21b3 No connector available. Run \`vana sources\``);
|
|
1278
|
-
}
|
|
1279
|
-
else if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
1280
|
-
const reason = source.lastError ?? "runtime error";
|
|
1281
|
-
emit.detail(` \u21b3 ${reason}. Run \`vana connect ${source.source}\``);
|
|
1282
|
-
}
|
|
1283
|
-
else if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
1284
|
-
emit.detail(` \u21b3 Requires interactive login. Run \`vana connect ${source.source}\``);
|
|
1285
|
-
}
|
|
1286
|
-
else if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
1287
|
-
emit.detail(` \u21b3 Manual auth step required. Run \`vana connect ${source.source}\``);
|
|
1288
|
-
}
|
|
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);
|
|
1289
1266
|
}
|
|
1290
1267
|
}
|
|
1291
1268
|
if (nextSteps.length > 0) {
|
|
@@ -1300,6 +1277,88 @@ async function runStatus(options) {
|
|
|
1300
1277
|
}
|
|
1301
1278
|
return 0;
|
|
1302
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
|
+
}
|
|
1303
1362
|
async function runDoctor(options) {
|
|
1304
1363
|
const payload = await queryDoctor();
|
|
1305
1364
|
if (options.json) {
|
|
@@ -1408,9 +1467,11 @@ async function runServerStatus(options) {
|
|
|
1408
1467
|
? "(auto-detected)"
|
|
1409
1468
|
: target.source === "config"
|
|
1410
1469
|
? "(saved)"
|
|
1411
|
-
: target.source === "
|
|
1412
|
-
? "(from
|
|
1413
|
-
:
|
|
1470
|
+
: target.source === "auth"
|
|
1471
|
+
? "(from vana login)"
|
|
1472
|
+
: target.source === "env"
|
|
1473
|
+
? "(from VANA_PERSONAL_SERVER_URL)"
|
|
1474
|
+
: `(${target.source ?? "unknown"})`;
|
|
1414
1475
|
emit.keyValue("URL", `${target.url} ${urlSuffix}`, "muted");
|
|
1415
1476
|
}
|
|
1416
1477
|
const stateLabel = target.state === "available" ? "healthy" : "Not connected";
|
|
@@ -1992,6 +2053,105 @@ async function runCollect(source, options) {
|
|
|
1992
2053
|
}
|
|
1993
2054
|
return runConnect(source, options);
|
|
1994
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
|
+
}
|
|
1995
2155
|
async function runCollectAll(options) {
|
|
1996
2156
|
const emit = createEmitter(options);
|
|
1997
2157
|
const state = await readCliState();
|
|
@@ -1999,22 +2159,37 @@ async function runCollectAll(options) {
|
|
|
1999
2159
|
.filter(([, stored]) => stored?.connectorInstalled &&
|
|
2000
2160
|
isCollectionDue(stored.exportFrequency, stored.lastCollectedAt))
|
|
2001
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
|
+
}
|
|
2002
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
|
+
}
|
|
2003
2185
|
if (options.json) {
|
|
2004
|
-
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`);
|
|
2005
2187
|
}
|
|
2006
2188
|
else {
|
|
2007
2189
|
emit.info("No sources are due for collection.");
|
|
2008
2190
|
}
|
|
2009
2191
|
return 0;
|
|
2010
2192
|
}
|
|
2011
|
-
let exitCode = 0;
|
|
2012
|
-
for (const source of dueSources) {
|
|
2013
|
-
const result = await runConnect(source, options);
|
|
2014
|
-
if (result !== 0) {
|
|
2015
|
-
exitCode = result;
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
2193
|
return exitCode;
|
|
2019
2194
|
}
|
|
2020
2195
|
async function runServerSync(options) {
|
|
@@ -2032,13 +2207,8 @@ async function runServerSync(options) {
|
|
|
2032
2207
|
}
|
|
2033
2208
|
return 1;
|
|
2034
2209
|
}
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
const pendingSources = Object.entries(state.sources).filter(([, stored]) => stored?.lastResultPath &&
|
|
2038
|
-
(stored.dataState === "collected_local" ||
|
|
2039
|
-
stored.dataState === "ingest_failed" ||
|
|
2040
|
-
stored?.ingestScopes?.some((s) => s.status === "failed")));
|
|
2041
|
-
if (pendingSources.length === 0) {
|
|
2210
|
+
const syncResult = await syncPendingSources(target, "manual");
|
|
2211
|
+
if (syncResult.sourceResults.length === 0) {
|
|
2042
2212
|
if (options.json) {
|
|
2043
2213
|
process.stdout.write(`${JSON.stringify({ message: "No pending datasets to sync.", syncedCount: 0 })}\n`);
|
|
2044
2214
|
}
|
|
@@ -2047,46 +2217,13 @@ async function runServerSync(options) {
|
|
|
2047
2217
|
}
|
|
2048
2218
|
return 0;
|
|
2049
2219
|
}
|
|
2050
|
-
let syncedCount = 0;
|
|
2051
|
-
const allScopeResults = [];
|
|
2052
|
-
for (const [source, stored] of pendingSources) {
|
|
2053
|
-
if (!stored?.lastResultPath) {
|
|
2054
|
-
continue;
|
|
2055
|
-
}
|
|
2056
|
-
const ingestEvents = await ingestResult(source, stored.lastResultPath, target);
|
|
2057
|
-
const resultEvent = ingestEvents.find((e) => e.type === "ingest-complete" ||
|
|
2058
|
-
e.type === "ingest-partial" ||
|
|
2059
|
-
e.type === "ingest-failed");
|
|
2060
|
-
const scopeResults = resultEvent?.scopeResults;
|
|
2061
|
-
const ingestCompleted = ingestEvents.some((e) => e.type === "ingest-complete");
|
|
2062
|
-
const ingestPartial = ingestEvents.some((e) => e.type === "ingest-partial");
|
|
2063
|
-
if (ingestCompleted || ingestPartial) {
|
|
2064
|
-
syncedCount++;
|
|
2065
|
-
const dataState = ingestCompleted || ingestPartial
|
|
2066
|
-
? "ingested_personal_server"
|
|
2067
|
-
: stored.dataState;
|
|
2068
|
-
await updateSourceState(source, {
|
|
2069
|
-
dataState,
|
|
2070
|
-
ingestScopes: scopeResults?.map((r) => ({
|
|
2071
|
-
scope: r.scope,
|
|
2072
|
-
status: r.status,
|
|
2073
|
-
syncedAt: r.status === "stored" ? new Date().toISOString() : undefined,
|
|
2074
|
-
error: r.error,
|
|
2075
|
-
})),
|
|
2076
|
-
});
|
|
2077
|
-
}
|
|
2078
|
-
allScopeResults.push({ source, scopeResults });
|
|
2079
|
-
for (const event of ingestEvents) {
|
|
2080
|
-
emit.event(event);
|
|
2081
|
-
}
|
|
2082
|
-
}
|
|
2083
2220
|
if (options.json) {
|
|
2084
|
-
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`);
|
|
2085
2222
|
}
|
|
2086
2223
|
else {
|
|
2087
2224
|
// Show per-scope results with scope manifest style
|
|
2088
2225
|
const renderer = createHumanRenderer();
|
|
2089
|
-
for (const entry of
|
|
2226
|
+
for (const entry of syncResult.sourceResults) {
|
|
2090
2227
|
if (entry.scopeResults && entry.scopeResults.length > 0) {
|
|
2091
2228
|
emit.info(`${entry.source}:`);
|
|
2092
2229
|
for (const sr of entry.scopeResults) {
|
|
@@ -2101,9 +2238,9 @@ async function runServerSync(options) {
|
|
|
2101
2238
|
}
|
|
2102
2239
|
}
|
|
2103
2240
|
emit.blank();
|
|
2104
|
-
const allStored =
|
|
2241
|
+
const allStored = syncResult.sourceResults.every((entry) => !entry.scopeResults ||
|
|
2105
2242
|
entry.scopeResults.every((sr) => sr.status === "stored"));
|
|
2106
|
-
emit.success(`Synced ${syncedCount} dataset(s).`);
|
|
2243
|
+
emit.success(`Synced ${syncResult.syncedCount} dataset(s).`);
|
|
2107
2244
|
emit.blank();
|
|
2108
2245
|
if (allStored) {
|
|
2109
2246
|
emit.next("vana data list");
|
|
@@ -2129,12 +2266,20 @@ async function runServerData(scope, options) {
|
|
|
2129
2266
|
}
|
|
2130
2267
|
// If PS is available, try to list remote scopes via client
|
|
2131
2268
|
let remoteScopes = [];
|
|
2269
|
+
let didQueryRemoteScopes = false;
|
|
2132
2270
|
let remoteScopeFallbackReason;
|
|
2133
2271
|
if (target.state === "available" && target.url) {
|
|
2272
|
+
const auth = resolvePersonalServerAuthConfig(target.url);
|
|
2134
2273
|
try {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
+
}
|
|
2138
2283
|
}
|
|
2139
2284
|
catch (err) {
|
|
2140
2285
|
remoteScopeFallbackReason =
|
|
@@ -2142,7 +2287,7 @@ async function runServerData(scope, options) {
|
|
|
2142
2287
|
}
|
|
2143
2288
|
}
|
|
2144
2289
|
// Use remote scopes if available, otherwise fall back to local
|
|
2145
|
-
const scopeList =
|
|
2290
|
+
const scopeList = didQueryRemoteScopes
|
|
2146
2291
|
? remoteScopes.map((s) => ({
|
|
2147
2292
|
scope: s.scope,
|
|
2148
2293
|
detail: `${s.count} version${s.count !== 1 ? "s" : ""}`,
|
|
@@ -2155,13 +2300,15 @@ async function runServerData(scope, options) {
|
|
|
2155
2300
|
process.stdout.write(`${JSON.stringify({
|
|
2156
2301
|
count: scopeList.length,
|
|
2157
2302
|
scopes: scopeList,
|
|
2158
|
-
source:
|
|
2303
|
+
source: didQueryRemoteScopes ? "remote" : "local",
|
|
2159
2304
|
...(remoteScopeFallbackReason ? { remoteScopeFallbackReason } : {}),
|
|
2160
2305
|
})}\n`);
|
|
2161
2306
|
return 0;
|
|
2162
2307
|
}
|
|
2163
2308
|
if (scopeList.length === 0) {
|
|
2164
|
-
emit.info(
|
|
2309
|
+
emit.info(didQueryRemoteScopes
|
|
2310
|
+
? "No data on your Personal Server."
|
|
2311
|
+
: "No scopes found.");
|
|
2165
2312
|
if (target.state !== "available") {
|
|
2166
2313
|
emit.detail("Personal Server is not available. Showing locally-known scopes only.");
|
|
2167
2314
|
}
|
|
@@ -2170,7 +2317,7 @@ async function runServerData(scope, options) {
|
|
|
2170
2317
|
for (const entry of scopeList) {
|
|
2171
2318
|
emit.keyValue(entry.scope, entry.detail, "muted");
|
|
2172
2319
|
}
|
|
2173
|
-
if (
|
|
2320
|
+
if (!didQueryRemoteScopes && localScopes.length > 0) {
|
|
2174
2321
|
emit.blank();
|
|
2175
2322
|
emit.detail("Showing locally-known scopes. Connect your Personal Server for live data.");
|
|
2176
2323
|
}
|
|
@@ -2358,6 +2505,62 @@ export function humanizeIssue(message) {
|
|
|
2358
2505
|
if (/checksum|mismatch/i.test(message)) {
|
|
2359
2506
|
return "Connector is out of date. Will auto-update on next connect.";
|
|
2360
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
|
+
}
|
|
2361
2564
|
return message;
|
|
2362
2565
|
}
|
|
2363
2566
|
function formatHumanSourceMessage(message, source, displayName) {
|
|
@@ -2391,11 +2594,13 @@ export async function gatherSourceStatuses(storedSources, metadata = {}) {
|
|
|
2391
2594
|
const details = metadata[source];
|
|
2392
2595
|
const dataState = stored.dataState === "ingested_personal_server"
|
|
2393
2596
|
? "ingested_personal_server"
|
|
2394
|
-
: stored.dataState === "
|
|
2395
|
-
? "
|
|
2396
|
-
: stored.dataState === "
|
|
2397
|
-
? "
|
|
2398
|
-
: "
|
|
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";
|
|
2399
2604
|
const ingestScopes = stored.ingestScopes;
|
|
2400
2605
|
const syncedScopeCount = ingestScopes?.filter((s) => s.status === "stored").length ?? 0;
|
|
2401
2606
|
const failedScopeCount = ingestScopes?.filter((s) => s.status === "failed").length ?? 0;
|
|
@@ -2554,6 +2759,14 @@ function formatSourceStatusDetails(source) {
|
|
|
2554
2759
|
tone: "success",
|
|
2555
2760
|
});
|
|
2556
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
|
+
}
|
|
2557
2770
|
else if (source.dataState === "ingest_failed") {
|
|
2558
2771
|
details.push({
|
|
2559
2772
|
kind: "row",
|
|
@@ -2609,11 +2822,15 @@ function formatSourceStatusDetails(source) {
|
|
|
2609
2822
|
}
|
|
2610
2823
|
export function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhealthy", availableSources = []) {
|
|
2611
2824
|
const nextSteps = [];
|
|
2612
|
-
const
|
|
2825
|
+
const attentionSources = [...sources]
|
|
2826
|
+
.filter(isSourceAttention)
|
|
2827
|
+
.sort(compareAttentionPriority);
|
|
2828
|
+
const highestPriority = attentionSources[0] ?? [...sources].sort(compareSourceStatusOrder)[0];
|
|
2613
2829
|
const connectedSources = sources.filter((source) => source.dataState === "collected_local" ||
|
|
2614
2830
|
source.dataState === "ingested_personal_server" ||
|
|
2615
|
-
source.dataState === "ingest_failed"
|
|
2616
|
-
|
|
2831
|
+
source.dataState === "ingest_failed" ||
|
|
2832
|
+
source.dataState === "ingest_unavailable");
|
|
2833
|
+
const needsAttention = attentionSources.length > 0;
|
|
2617
2834
|
const highestPriorityLabel = highestPriority
|
|
2618
2835
|
? displaySource(highestPriority.source, sourceLabels)
|
|
2619
2836
|
: null;
|
|
@@ -2636,8 +2853,22 @@ export function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhe
|
|
|
2636
2853
|
nextSteps.push("Inspect install health with `vana doctor`.");
|
|
2637
2854
|
}
|
|
2638
2855
|
}
|
|
2639
|
-
else if (highestPriority.
|
|
2640
|
-
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}\`.`);
|
|
2641
2872
|
if (highestPriority.lastLogPath) {
|
|
2642
2873
|
nextSteps.push(`Inspect the latest run log with \`vana logs ${highestPriority.source}\`.`);
|
|
2643
2874
|
}
|
|
@@ -2655,8 +2886,7 @@ export function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhe
|
|
|
2655
2886
|
}
|
|
2656
2887
|
}
|
|
2657
2888
|
else if (highestPriority.dataState === "collected_local" ||
|
|
2658
|
-
highestPriority.dataState === "ingested_personal_server"
|
|
2659
|
-
highestPriority.dataState === "ingest_failed") {
|
|
2889
|
+
highestPriority.dataState === "ingested_personal_server") {
|
|
2660
2890
|
if (connectedSources.length > 1) {
|
|
2661
2891
|
nextSteps.push("Review your collected data with `vana data list`.");
|
|
2662
2892
|
}
|
|
@@ -2961,6 +3191,9 @@ export function getSourceStatusPresentation(source) {
|
|
|
2961
3191
|
}
|
|
2962
3192
|
return { label: "synced", tone: "success" };
|
|
2963
3193
|
}
|
|
3194
|
+
if (source.dataState === "ingest_unavailable") {
|
|
3195
|
+
return { label: "pending sync", tone: "warning" };
|
|
3196
|
+
}
|
|
2964
3197
|
if (source.dataState === "collected_local") {
|
|
2965
3198
|
return { label: "local", tone: "muted" };
|
|
2966
3199
|
}
|
|
@@ -3050,6 +3283,48 @@ export function compareSourceStatusOrder(left, right) {
|
|
|
3050
3283
|
authMode: right.authMode,
|
|
3051
3284
|
}));
|
|
3052
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
|
+
}
|
|
3053
3328
|
export function rankSourceStatus(source) {
|
|
3054
3329
|
if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
3055
3330
|
return 0;
|
|
@@ -3189,6 +3464,7 @@ function compareLogRecordOrder(left, right) {
|
|
|
3189
3464
|
}
|
|
3190
3465
|
export function hasCollectedData(dataState) {
|
|
3191
3466
|
return (dataState === "collected_local" ||
|
|
3467
|
+
dataState === "ingest_unavailable" ||
|
|
3192
3468
|
dataState === "ingested_personal_server" ||
|
|
3193
3469
|
dataState === "ingest_failed");
|
|
3194
3470
|
}
|
|
@@ -3208,6 +3484,9 @@ function formatLogOutcomeLabel(lastRunOutcome, dataState) {
|
|
|
3208
3484
|
if (dataState === "ingested_personal_server") {
|
|
3209
3485
|
return "synced";
|
|
3210
3486
|
}
|
|
3487
|
+
if (dataState === "ingest_unavailable") {
|
|
3488
|
+
return "pending sync";
|
|
3489
|
+
}
|
|
3211
3490
|
if (dataState === "ingest_failed") {
|
|
3212
3491
|
return "sync failed";
|
|
3213
3492
|
}
|
|
@@ -3229,7 +3508,8 @@ function toneForLogOutcome(lastRunOutcome, dataState) {
|
|
|
3229
3508
|
if (lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE ||
|
|
3230
3509
|
lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH ||
|
|
3231
3510
|
lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT ||
|
|
3232
|
-
dataState === "ingest_failed"
|
|
3511
|
+
dataState === "ingest_failed" ||
|
|
3512
|
+
dataState === "ingest_unavailable") {
|
|
3233
3513
|
return "warning";
|
|
3234
3514
|
}
|
|
3235
3515
|
if (dataState === "ingested_personal_server") {
|
|
@@ -3240,20 +3520,6 @@ function toneForLogOutcome(lastRunOutcome, dataState) {
|
|
|
3240
3520
|
}
|
|
3241
3521
|
return "muted";
|
|
3242
3522
|
}
|
|
3243
|
-
function toneForHealth(health) {
|
|
3244
|
-
switch (health) {
|
|
3245
|
-
case "healthy":
|
|
3246
|
-
return "accent";
|
|
3247
|
-
case "needs_reauth":
|
|
3248
|
-
return "warning";
|
|
3249
|
-
case "error":
|
|
3250
|
-
return "error";
|
|
3251
|
-
case "stale":
|
|
3252
|
-
return "muted";
|
|
3253
|
-
default:
|
|
3254
|
-
return "muted";
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
3523
|
// ---------------------------------------------------------------------------
|
|
3258
3524
|
// Detach (background process)
|
|
3259
3525
|
// ---------------------------------------------------------------------------
|
|
@@ -3959,4 +4225,195 @@ async function runSkillShow(name, options) {
|
|
|
3959
4225
|
return 1;
|
|
3960
4226
|
}
|
|
3961
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
|
+
}
|
|
3962
4419
|
//# sourceMappingURL=index.js.map
|