@opendatalabs/connect 0.8.0 → 0.8.1-canary.03023c2
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 +78 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +15 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +2741 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/render/capabilities.d.ts +9 -0
- package/dist/cli/render/capabilities.d.ts.map +1 -0
- package/dist/cli/render/capabilities.js +24 -0
- package/dist/cli/render/capabilities.js.map +1 -0
- package/dist/cli/render/format.d.ts +22 -0
- package/dist/cli/render/format.d.ts.map +1 -0
- package/dist/cli/render/format.js +77 -0
- package/dist/cli/render/format.js.map +1 -0
- package/dist/cli/render/index.d.ts +6 -0
- package/dist/cli/render/index.d.ts.map +1 -0
- package/dist/cli/render/index.js +6 -0
- package/dist/cli/render/index.js.map +1 -0
- package/dist/cli/render/progress.d.ts +11 -0
- package/dist/cli/render/progress.d.ts.map +1 -0
- package/dist/cli/render/progress.js +56 -0
- package/dist/cli/render/progress.js.map +1 -0
- package/dist/cli/render/symbols.d.ts +11 -0
- package/dist/cli/render/symbols.d.ts.map +1 -0
- package/dist/cli/render/symbols.js +21 -0
- package/dist/cli/render/symbols.js.map +1 -0
- package/dist/cli/render/theme.d.ts +15 -0
- package/dist/cli/render/theme.d.ts.map +1 -0
- package/dist/cli/render/theme.js +41 -0
- package/dist/cli/render/theme.js.map +1 -0
- package/dist/connectors/index.d.ts +2 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +2 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/registry.d.ts +30 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +166 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/core/cli-types.d.ts +595 -0
- package/dist/core/cli-types.d.ts.map +1 -0
- package/dist/core/cli-types.js +284 -0
- package/dist/core/cli-types.js.map +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/paths.d.ts +8 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +25 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/state-store.d.ts +22 -0
- package/dist/core/state-store.d.ts.map +1 -0
- package/dist/core/state-store.js +87 -0
- package/dist/core/state-store.js.map +1 -0
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/personal-server/index.d.ts +8 -0
- package/dist/personal-server/index.d.ts.map +1 -0
- package/dist/personal-server/index.js +76 -0
- package/dist/personal-server/index.js.map +1 -0
- package/dist/react/useVanaData.d.ts +1 -1
- package/dist/react/useVanaData.d.ts.map +1 -1
- package/dist/react/useVanaData.js +22 -9
- package/dist/react/useVanaData.js.map +1 -1
- package/dist/runtime/core/contracts.d.ts +83 -0
- package/dist/runtime/core/contracts.d.ts.map +1 -0
- package/dist/runtime/core/contracts.js +2 -0
- package/dist/runtime/core/contracts.js.map +1 -0
- package/dist/runtime/core/index.d.ts +2 -0
- package/dist/runtime/core/index.d.ts.map +1 -0
- package/dist/runtime/core/index.js +2 -0
- package/dist/runtime/core/index.js.map +1 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/managed-playwright.d.ts +38 -0
- package/dist/runtime/managed-playwright.d.ts.map +1 -0
- package/dist/runtime/managed-playwright.js +172 -0
- package/dist/runtime/managed-playwright.js.map +1 -0
- package/dist/runtime/playwright/browser.d.ts +12 -0
- package/dist/runtime/playwright/browser.d.ts.map +1 -0
- package/dist/runtime/playwright/browser.js +218 -0
- package/dist/runtime/playwright/browser.js.map +1 -0
- package/dist/runtime/playwright/in-process-run.d.ts +6 -0
- package/dist/runtime/playwright/in-process-run.d.ts.map +1 -0
- package/dist/runtime/playwright/in-process-run.js +576 -0
- package/dist/runtime/playwright/in-process-run.js.map +1 -0
- package/dist/runtime/playwright/index.d.ts +3 -0
- package/dist/runtime/playwright/index.d.ts.map +1 -0
- package/dist/runtime/playwright/index.js +3 -0
- package/dist/runtime/playwright/index.js.map +1 -0
- package/dist/runtime/repo-paths.d.ts +2 -0
- package/dist/runtime/repo-paths.d.ts.map +1 -0
- package/dist/runtime/repo-paths.js +36 -0
- package/dist/runtime/repo-paths.js.map +1 -0
- package/dist/server/config.d.ts +2 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +1 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/connect.d.ts.map +1 -1
- package/dist/server/connect.js +7 -0
- package/dist/server/connect.js.map +1 -1
- package/package.json +48 -4
|
@@ -0,0 +1,2741 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { Separator, confirm, input, password, select } from "@inquirer/prompts";
|
|
6
|
+
import { Command, CommanderError } from "commander";
|
|
7
|
+
import { createHumanRenderer, createProgressHandle, formatDisplayPath, } from "./render/index.js";
|
|
8
|
+
import { CliOutcomeStatus, getCliStatePath, getBrowserProfilesDir, getConnectorCacheDir, getDataConnectHome, getLastResultPath, getLogsDir, readCliState, updateSourceState, } from "../core/index.js";
|
|
9
|
+
import { listAvailableSources } from "../connectors/registry.js";
|
|
10
|
+
import { detectPersonalServerTarget, ingestResult, } from "../personal-server/index.js";
|
|
11
|
+
import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/index.js";
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
export async function runCli(argv = process.argv) {
|
|
14
|
+
const normalizedArgv = normalizeArgv(argv);
|
|
15
|
+
if (normalizedArgv.length <= 2) {
|
|
16
|
+
normalizedArgv.push("--help");
|
|
17
|
+
}
|
|
18
|
+
const parsedOptions = extractGlobalOptions(normalizedArgv);
|
|
19
|
+
const cliVersion = getCliVersion();
|
|
20
|
+
const program = new Command();
|
|
21
|
+
program
|
|
22
|
+
.name("vana")
|
|
23
|
+
.description("Connect sources, collect data, and inspect it locally.")
|
|
24
|
+
.version(cliVersion, "-v, --version", "Print CLI version")
|
|
25
|
+
.addHelpText("after", `
|
|
26
|
+
Start here:
|
|
27
|
+
vana connect
|
|
28
|
+
vana status
|
|
29
|
+
vana data list
|
|
30
|
+
|
|
31
|
+
Automation:
|
|
32
|
+
vana connect github --json --no-input
|
|
33
|
+
vana sources --json | jq '.sources[] | {id, authMode}'
|
|
34
|
+
|
|
35
|
+
Support:
|
|
36
|
+
vana doctor
|
|
37
|
+
vana logs
|
|
38
|
+
|
|
39
|
+
Version:
|
|
40
|
+
${cliVersion} (${getCliChannel(cliVersion)}, ${formatInstallMethodLabel(getCliInstallMethod()).toLowerCase()})
|
|
41
|
+
`);
|
|
42
|
+
program.exitOverride();
|
|
43
|
+
program
|
|
44
|
+
.command("version")
|
|
45
|
+
.description("Print CLI version")
|
|
46
|
+
.option("--json", "Output machine-readable JSON")
|
|
47
|
+
.action(async () => {
|
|
48
|
+
if (parsedOptions.json) {
|
|
49
|
+
process.stdout.write(`${JSON.stringify({
|
|
50
|
+
cliVersion,
|
|
51
|
+
channel: getCliChannel(cliVersion),
|
|
52
|
+
installMethod: getCliInstallMethod(),
|
|
53
|
+
})}\n`);
|
|
54
|
+
process.exitCode = 0;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
process.stdout.write(`${cliVersion} (${getCliChannel(cliVersion)}, ${formatInstallMethodLabel(getCliInstallMethod()).toLowerCase()})\n`);
|
|
58
|
+
process.exitCode = 0;
|
|
59
|
+
});
|
|
60
|
+
const connectCommand = program
|
|
61
|
+
.command("connect [source]")
|
|
62
|
+
.description("Connect a source and collect data")
|
|
63
|
+
.option("--json", "Output machine-readable JSON")
|
|
64
|
+
.option("--no-input", "Fail instead of prompting for input")
|
|
65
|
+
.option("--yes", "Approve safe setup prompts automatically")
|
|
66
|
+
.option("--quiet", "Reduce non-essential output")
|
|
67
|
+
.action(async (source) => {
|
|
68
|
+
process.exitCode = source
|
|
69
|
+
? await runConnect(source, parsedOptions)
|
|
70
|
+
: await runConnectEntry(parsedOptions);
|
|
71
|
+
});
|
|
72
|
+
connectCommand.addHelpText("after", `
|
|
73
|
+
Examples:
|
|
74
|
+
vana connect
|
|
75
|
+
vana connect github
|
|
76
|
+
vana connect github --json --no-input
|
|
77
|
+
`);
|
|
78
|
+
const sourcesCommand = program
|
|
79
|
+
.command("sources")
|
|
80
|
+
.description("List supported sources")
|
|
81
|
+
.option("--json", "Output machine-readable JSON")
|
|
82
|
+
.action(async () => {
|
|
83
|
+
process.exitCode = await runList(parsedOptions);
|
|
84
|
+
});
|
|
85
|
+
sourcesCommand.addHelpText("after", `
|
|
86
|
+
Examples:
|
|
87
|
+
vana sources
|
|
88
|
+
vana sources --json | jq '.sources'
|
|
89
|
+
`);
|
|
90
|
+
const statusCommand = program
|
|
91
|
+
.command("status")
|
|
92
|
+
.description("Show runtime and Personal Server status")
|
|
93
|
+
.option("--json", "Output machine-readable JSON")
|
|
94
|
+
.action(async () => {
|
|
95
|
+
process.exitCode = await runStatus(parsedOptions);
|
|
96
|
+
});
|
|
97
|
+
statusCommand.addHelpText("after", `
|
|
98
|
+
Examples:
|
|
99
|
+
vana status
|
|
100
|
+
vana status --json | jq
|
|
101
|
+
`);
|
|
102
|
+
const doctorCommand = program
|
|
103
|
+
.command("doctor")
|
|
104
|
+
.description("Inspect local CLI, runtime, and install health")
|
|
105
|
+
.option("--json", "Output machine-readable JSON")
|
|
106
|
+
.action(async () => {
|
|
107
|
+
process.exitCode = await runDoctor(parsedOptions);
|
|
108
|
+
});
|
|
109
|
+
doctorCommand.addHelpText("after", `
|
|
110
|
+
Examples:
|
|
111
|
+
vana doctor
|
|
112
|
+
vana doctor --json | jq
|
|
113
|
+
`);
|
|
114
|
+
const setupCommand = program
|
|
115
|
+
.command("setup")
|
|
116
|
+
.description("Install or repair the local runtime")
|
|
117
|
+
.option("--json", "Output machine-readable JSON")
|
|
118
|
+
.option("--yes", "Approve safe setup prompts automatically")
|
|
119
|
+
.action(async () => {
|
|
120
|
+
process.exitCode = await runSetup(parsedOptions);
|
|
121
|
+
});
|
|
122
|
+
setupCommand.addHelpText("after", `
|
|
123
|
+
Examples:
|
|
124
|
+
vana setup
|
|
125
|
+
vana setup --yes
|
|
126
|
+
`);
|
|
127
|
+
const data = program
|
|
128
|
+
.command("data")
|
|
129
|
+
.description("Inspect collected datasets, paths, and summaries");
|
|
130
|
+
data.addHelpText("after", `
|
|
131
|
+
Examples:
|
|
132
|
+
vana data list
|
|
133
|
+
vana data show github
|
|
134
|
+
vana data path github --json
|
|
135
|
+
`);
|
|
136
|
+
data.action(() => {
|
|
137
|
+
data.outputHelp();
|
|
138
|
+
process.exitCode = 0;
|
|
139
|
+
});
|
|
140
|
+
const dataListCommand = data
|
|
141
|
+
.command("list")
|
|
142
|
+
.description("List locally available collected datasets")
|
|
143
|
+
.option("--json", "Output machine-readable JSON")
|
|
144
|
+
.action(async () => {
|
|
145
|
+
process.exitCode = await runDataList(parsedOptions);
|
|
146
|
+
});
|
|
147
|
+
dataListCommand.addHelpText("after", `
|
|
148
|
+
Examples:
|
|
149
|
+
vana data list
|
|
150
|
+
vana data list --json | jq '.datasets'
|
|
151
|
+
`);
|
|
152
|
+
const dataShowCommand = data
|
|
153
|
+
.command("show <source>")
|
|
154
|
+
.description("Show a collected dataset")
|
|
155
|
+
.option("--json", "Output machine-readable JSON")
|
|
156
|
+
.action(async (source) => {
|
|
157
|
+
process.exitCode = await runDataShow(source, parsedOptions);
|
|
158
|
+
});
|
|
159
|
+
dataShowCommand.addHelpText("after", `
|
|
160
|
+
Examples:
|
|
161
|
+
vana data show github
|
|
162
|
+
vana data show github --json | jq '.summary'
|
|
163
|
+
`);
|
|
164
|
+
const dataPathCommand = data
|
|
165
|
+
.command("path <source>")
|
|
166
|
+
.description("Print the local path for a collected dataset")
|
|
167
|
+
.option("--json", "Output machine-readable JSON")
|
|
168
|
+
.action(async (source) => {
|
|
169
|
+
process.exitCode = await runDataPath(source, parsedOptions);
|
|
170
|
+
});
|
|
171
|
+
dataPathCommand.addHelpText("after", `
|
|
172
|
+
Examples:
|
|
173
|
+
vana data path github
|
|
174
|
+
vana data path github --json | jq -r '.path'
|
|
175
|
+
`);
|
|
176
|
+
const logsCommand = program
|
|
177
|
+
.command("logs [source]")
|
|
178
|
+
.description("Inspect stored connector run logs")
|
|
179
|
+
.option("--json", "Output machine-readable JSON")
|
|
180
|
+
.action(async (source) => {
|
|
181
|
+
process.exitCode = await runLogs(source, parsedOptions);
|
|
182
|
+
});
|
|
183
|
+
logsCommand.addHelpText("after", `
|
|
184
|
+
Examples:
|
|
185
|
+
vana logs
|
|
186
|
+
vana logs github
|
|
187
|
+
vana logs github --json | jq
|
|
188
|
+
`);
|
|
189
|
+
try {
|
|
190
|
+
await program.parseAsync(normalizedArgv);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof CommanderError &&
|
|
194
|
+
(error.code === "commander.help" ||
|
|
195
|
+
error.code === "commander.helpDisplayed" ||
|
|
196
|
+
error.code === "commander.version")) {
|
|
197
|
+
process.exitCode = error.exitCode;
|
|
198
|
+
return Number(process.exitCode ?? 0);
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
return Number(process.exitCode ?? 0);
|
|
203
|
+
}
|
|
204
|
+
async function runConnect(source, options) {
|
|
205
|
+
const runtime = new ManagedPlaywrightRuntime();
|
|
206
|
+
const emit = createEmitter(options);
|
|
207
|
+
const progress = createProgressHandle({
|
|
208
|
+
enabled: !options.json && !options.quiet,
|
|
209
|
+
});
|
|
210
|
+
const registrySources = await loadRegistrySources();
|
|
211
|
+
const sourceLabels = createSourceLabelMap(registrySources);
|
|
212
|
+
const displayName = displaySource(source, sourceLabels);
|
|
213
|
+
let setupLogPath;
|
|
214
|
+
let fetchLogPath;
|
|
215
|
+
let runLogPath;
|
|
216
|
+
let terminalExitCode = null;
|
|
217
|
+
try {
|
|
218
|
+
emit.title(`Connect ${displayName}`);
|
|
219
|
+
emit.blank();
|
|
220
|
+
emit.section("Preparing");
|
|
221
|
+
emit.info(`Finding a connector for ${displayName}...`);
|
|
222
|
+
progress.start(`Preparing ${displayName}...`);
|
|
223
|
+
const target = await detectPersonalServerTarget();
|
|
224
|
+
if (runtime.state !== "installed") {
|
|
225
|
+
emit.info(`Vana Connect needs a local browser runtime before it can connect ${displayName}.`);
|
|
226
|
+
emit.blank();
|
|
227
|
+
emit.section("Runtime setup");
|
|
228
|
+
emit.bullet("Install the local browser runtime.");
|
|
229
|
+
emit.bullet("Install a Chromium browser engine.");
|
|
230
|
+
emit.bullet("Create local runtime files under `~/.dataconnect/`.");
|
|
231
|
+
emit.detail("Your credentials stay on this machine. Nothing is sent anywhere except the platform you’re connecting to.");
|
|
232
|
+
if (!options.yes) {
|
|
233
|
+
if (options.noInput) {
|
|
234
|
+
emit.event({
|
|
235
|
+
type: "outcome",
|
|
236
|
+
status: CliOutcomeStatus.SETUP_REQUIRED,
|
|
237
|
+
source,
|
|
238
|
+
});
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
const shouldContinue = await confirm({
|
|
242
|
+
message: "Install the local runtime now?",
|
|
243
|
+
default: true,
|
|
244
|
+
});
|
|
245
|
+
if (!shouldContinue) {
|
|
246
|
+
emit.info("Cancelled. Runtime setup was not started.");
|
|
247
|
+
emit.event({
|
|
248
|
+
type: "outcome",
|
|
249
|
+
status: CliOutcomeStatus.SETUP_REQUIRED,
|
|
250
|
+
source,
|
|
251
|
+
reason: "setup_declined",
|
|
252
|
+
});
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const installResult = await runtime.ensureInstalled(Boolean(options.yes));
|
|
257
|
+
setupLogPath = installResult.logPath;
|
|
258
|
+
emit.event({
|
|
259
|
+
type: "setup-complete",
|
|
260
|
+
runtime: installResult.runtime,
|
|
261
|
+
logPath: installResult.logPath,
|
|
262
|
+
});
|
|
263
|
+
emit.success("Runtime ready.");
|
|
264
|
+
progress.update("Runtime ready.");
|
|
265
|
+
if (installResult.logPath) {
|
|
266
|
+
emit.detail(`Setup log: ${formatDisplayPath(installResult.logPath)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
emit.event({
|
|
271
|
+
type: "setup-check",
|
|
272
|
+
runtime: runtime.state,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
let fetched;
|
|
276
|
+
try {
|
|
277
|
+
fetched = await runtime.fetchConnector(source);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const rawMessage = error instanceof Error
|
|
281
|
+
? error.message
|
|
282
|
+
: `No connector is available for ${displayName} right now.`;
|
|
283
|
+
const message = formatHumanSourceMessage(rawMessage, source, displayName);
|
|
284
|
+
await updateSourceState(source, {
|
|
285
|
+
connectorInstalled: false,
|
|
286
|
+
lastRunAt: new Date().toISOString(),
|
|
287
|
+
lastRunOutcome: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
|
|
288
|
+
dataState: "none",
|
|
289
|
+
lastError: message,
|
|
290
|
+
lastResultPath: null,
|
|
291
|
+
lastLogPath: getErrorLogPath(error),
|
|
292
|
+
});
|
|
293
|
+
if (!options.json) {
|
|
294
|
+
progress.fail(`${displayName} is not available yet.`);
|
|
295
|
+
const suggestedSource = registrySources.find((item) => item.authMode !== "legacy") ??
|
|
296
|
+
registrySources[0];
|
|
297
|
+
emit.blank();
|
|
298
|
+
emit.section("Not available yet");
|
|
299
|
+
emit.info(message);
|
|
300
|
+
emit.blank();
|
|
301
|
+
emit.section("Next");
|
|
302
|
+
if (suggestedSource) {
|
|
303
|
+
emit.bullet(`Try ${suggestedSource.name} with ${emit.code(`vana connect ${suggestedSource.id}`)}.`);
|
|
304
|
+
}
|
|
305
|
+
emit.bullet(`Browse available sources with ${emit.code("vana sources")}.`);
|
|
306
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
emit.info(message);
|
|
310
|
+
}
|
|
311
|
+
emit.event({
|
|
312
|
+
type: "outcome",
|
|
313
|
+
status: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
|
|
314
|
+
source,
|
|
315
|
+
reason: message,
|
|
316
|
+
});
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
fetchLogPath = fetched.logPath;
|
|
320
|
+
const sourceDetails = registrySources.find((item) => item.id === source);
|
|
321
|
+
const resolution = {
|
|
322
|
+
source,
|
|
323
|
+
connectorPath: fetched.connectorPath,
|
|
324
|
+
};
|
|
325
|
+
emit.event({
|
|
326
|
+
type: "connector-resolved",
|
|
327
|
+
source: resolution.source,
|
|
328
|
+
connectorPath: resolution.connectorPath,
|
|
329
|
+
logPath: fetched.logPath,
|
|
330
|
+
});
|
|
331
|
+
emit.info("Connector ready.");
|
|
332
|
+
progress.update(`Connector ready for ${displayName}.`);
|
|
333
|
+
if (sourceDetails?.description) {
|
|
334
|
+
emit.info(sourceDetails.description);
|
|
335
|
+
}
|
|
336
|
+
const connectTrustMessage = describeConnectTrust(sourceDetails?.authMode);
|
|
337
|
+
if (connectTrustMessage) {
|
|
338
|
+
emit.detail(connectTrustMessage);
|
|
339
|
+
}
|
|
340
|
+
const profilePath = path.join(getBrowserProfilesDir(), `${path.basename(resolution.connectorPath, path.extname(resolution.connectorPath))}`);
|
|
341
|
+
if (fs.existsSync(profilePath)) {
|
|
342
|
+
emit.detail(`Found an existing ${displayName} session. Reusing it if it is still valid...`);
|
|
343
|
+
}
|
|
344
|
+
if (sourceDetails?.authMode === "legacy" &&
|
|
345
|
+
!options.noInput &&
|
|
346
|
+
process.platform === "linux" &&
|
|
347
|
+
!process.env.DISPLAY &&
|
|
348
|
+
!process.env.WAYLAND_DISPLAY) {
|
|
349
|
+
const message = "This source needs a manual browser step, but no local display server is available. Run this command in a desktop session or use xvfb-run.";
|
|
350
|
+
await updateSourceState(resolution.source, {
|
|
351
|
+
connectorInstalled: true,
|
|
352
|
+
sessionPresent: fs.existsSync(profilePath),
|
|
353
|
+
lastRunAt: new Date().toISOString(),
|
|
354
|
+
lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
|
|
355
|
+
dataState: "none",
|
|
356
|
+
lastError: message,
|
|
357
|
+
lastResultPath: null,
|
|
358
|
+
lastLogPath: fetchLogPath ?? null,
|
|
359
|
+
});
|
|
360
|
+
emit.blank();
|
|
361
|
+
emit.section("Manual step required");
|
|
362
|
+
emit.info(`${displayName} still needs a manual browser step on this machine.`);
|
|
363
|
+
emit.detail(message);
|
|
364
|
+
emit.blank();
|
|
365
|
+
emit.section("Next");
|
|
366
|
+
emit.bullet("Run this command in a desktop session.");
|
|
367
|
+
emit.bullet(`Or retry with ${emit.code(`xvfb-run -a vana connect ${source}`)}.`);
|
|
368
|
+
if (fetchLogPath) {
|
|
369
|
+
emit.bullet(`Inspect the latest run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
370
|
+
}
|
|
371
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
372
|
+
emit.event({
|
|
373
|
+
type: "outcome",
|
|
374
|
+
status: CliOutcomeStatus.LEGACY_AUTH,
|
|
375
|
+
source: resolution.source,
|
|
376
|
+
reason: "display_server_unavailable",
|
|
377
|
+
});
|
|
378
|
+
progress.fail(`Manual step required for ${displayName}.`);
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
await updateSourceState(resolution.source, {
|
|
382
|
+
connectorInstalled: true,
|
|
383
|
+
sessionPresent: fs.existsSync(profilePath),
|
|
384
|
+
lastError: null,
|
|
385
|
+
lastLogPath: fetchLogPath ?? null,
|
|
386
|
+
});
|
|
387
|
+
emit.blank();
|
|
388
|
+
emit.section("Connecting");
|
|
389
|
+
emit.info(`Connecting to ${displayName}...`);
|
|
390
|
+
emit.info("Collecting your data...");
|
|
391
|
+
progress.update(`Collecting ${displayName} data...`);
|
|
392
|
+
let finalStatus = CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR;
|
|
393
|
+
let finalDataState = "none";
|
|
394
|
+
let ingestFailureMessage = null;
|
|
395
|
+
let resultPath = getLastResultPath();
|
|
396
|
+
let collectedResult = false;
|
|
397
|
+
for await (const event of runtime.runConnector({
|
|
398
|
+
connectorPath: resolution.connectorPath,
|
|
399
|
+
source: resolution.source,
|
|
400
|
+
noInput: options.noInput,
|
|
401
|
+
onNeedInput: async (needInput) => {
|
|
402
|
+
emit.blank();
|
|
403
|
+
emit.section("Continue in this terminal");
|
|
404
|
+
emit.info(`Vana Connect will keep the ${displayName} session local to this machine.`);
|
|
405
|
+
emit.detail("The details you enter here stay local.");
|
|
406
|
+
emit.blank();
|
|
407
|
+
emit.info(needInput.message ??
|
|
408
|
+
`${displayName} needs additional details to continue.`);
|
|
409
|
+
const values = {};
|
|
410
|
+
try {
|
|
411
|
+
for (const field of needInput.fields) {
|
|
412
|
+
if (field.toLowerCase().includes("password")) {
|
|
413
|
+
values[field] = await password({ message: humanizeField(field) });
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
values[field] = await input({ message: humanizeField(field) });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
if (isPromptCancelled(error)) {
|
|
422
|
+
throw new Error("__vana_prompt_cancelled__");
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
return values;
|
|
427
|
+
},
|
|
428
|
+
})) {
|
|
429
|
+
emit.event(event);
|
|
430
|
+
if (event.logPath) {
|
|
431
|
+
runLogPath = event.logPath;
|
|
432
|
+
}
|
|
433
|
+
if (terminalExitCode !== null) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (event.type === "needs-input") {
|
|
437
|
+
await updateSourceState(resolution.source, {
|
|
438
|
+
lastRunAt: new Date().toISOString(),
|
|
439
|
+
lastRunOutcome: CliOutcomeStatus.NEEDS_INPUT,
|
|
440
|
+
lastError: event.message ?? "Input required.",
|
|
441
|
+
lastLogPath: event.logPath,
|
|
442
|
+
});
|
|
443
|
+
emit.event({
|
|
444
|
+
type: "outcome",
|
|
445
|
+
status: CliOutcomeStatus.NEEDS_INPUT,
|
|
446
|
+
source: resolution.source,
|
|
447
|
+
});
|
|
448
|
+
if (!options.json) {
|
|
449
|
+
progress.stop();
|
|
450
|
+
emit.blank();
|
|
451
|
+
emit.section("Input required");
|
|
452
|
+
emit.info(`${displayName} needs additional input before it can connect.`);
|
|
453
|
+
emit.detail(`Because ${emit.code("--no-input")} is enabled, Vana stopped before prompting in this terminal.`);
|
|
454
|
+
emit.blank();
|
|
455
|
+
emit.section("Next");
|
|
456
|
+
emit.bullet(`Run ${emit.code(`vana connect ${source}`)} without ${emit.code("--no-input")}.`);
|
|
457
|
+
if (event.logPath || fetchLogPath) {
|
|
458
|
+
emit.bullet(`Inspect the latest run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
459
|
+
}
|
|
460
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
461
|
+
}
|
|
462
|
+
terminalExitCode = 1;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (event.type === "progress-update") {
|
|
466
|
+
const progressLine = formatProgressUpdate(event);
|
|
467
|
+
if (progressLine) {
|
|
468
|
+
progress.update(progressLine);
|
|
469
|
+
emit.detail(progressLine);
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (event.type === "status-update") {
|
|
474
|
+
if (event.message && shouldRenderStatusUpdate(event.message)) {
|
|
475
|
+
progress.update(event.message);
|
|
476
|
+
emit.detail(event.message);
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (event.type === "runtime-error") {
|
|
481
|
+
await updateSourceState(resolution.source, {
|
|
482
|
+
lastRunAt: new Date().toISOString(),
|
|
483
|
+
lastRunOutcome: CliOutcomeStatus.RUNTIME_ERROR,
|
|
484
|
+
lastError: event.message ?? "Connector run failed.",
|
|
485
|
+
lastLogPath: event.logPath,
|
|
486
|
+
});
|
|
487
|
+
emit.blank();
|
|
488
|
+
progress.fail(`Problem connecting ${displayName}.`);
|
|
489
|
+
emit.section("Problem");
|
|
490
|
+
emit.info(event.message ?? "Connector run failed.");
|
|
491
|
+
emit.event({
|
|
492
|
+
type: "outcome",
|
|
493
|
+
status: CliOutcomeStatus.RUNTIME_ERROR,
|
|
494
|
+
source: resolution.source,
|
|
495
|
+
});
|
|
496
|
+
emit.blank();
|
|
497
|
+
emit.section("Next");
|
|
498
|
+
emit.bullet(`Retry with ${emit.code(`vana connect ${source}`)}.`);
|
|
499
|
+
if (event.logPath || fetchLogPath || setupLogPath) {
|
|
500
|
+
emit.bullet(`Inspect the latest run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
501
|
+
}
|
|
502
|
+
emit.bullet(`Inspect install health with ${emit.code("vana doctor")}.`);
|
|
503
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
504
|
+
if (event.logPath) {
|
|
505
|
+
emit.keyValue("Run log", formatDisplayPath(event.logPath), "muted");
|
|
506
|
+
}
|
|
507
|
+
terminalExitCode = 1;
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (event.type === "headed-required") {
|
|
511
|
+
emit.blank();
|
|
512
|
+
progress.update(`Manual browser step required for ${displayName}.`);
|
|
513
|
+
emit.section("Continue in your browser");
|
|
514
|
+
if (event.message) {
|
|
515
|
+
emit.info(event.message);
|
|
516
|
+
}
|
|
517
|
+
if (event.url) {
|
|
518
|
+
emit.detail(`Opening ${displayName} in a local browser session...`);
|
|
519
|
+
}
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (event.type === "legacy-auth") {
|
|
523
|
+
await updateSourceState(resolution.source, {
|
|
524
|
+
lastRunAt: new Date().toISOString(),
|
|
525
|
+
lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
|
|
526
|
+
lastError: event.message ?? "Legacy authentication is required.",
|
|
527
|
+
dataState: "none",
|
|
528
|
+
lastResultPath: null,
|
|
529
|
+
lastLogPath: event.logPath,
|
|
530
|
+
});
|
|
531
|
+
emit.blank();
|
|
532
|
+
progress.stop();
|
|
533
|
+
emit.section("Manual step required");
|
|
534
|
+
emit.info(`${displayName} still needs a manual browser step on this machine.`);
|
|
535
|
+
if (options.noInput) {
|
|
536
|
+
emit.detail(`Because ${emit.code("--no-input")} is enabled, Vana stopped before opening that session.`);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
emit.detail("Vana Connect could not continue this older connector flow automatically yet.");
|
|
540
|
+
}
|
|
541
|
+
emit.blank();
|
|
542
|
+
emit.section("Next");
|
|
543
|
+
if (options.noInput) {
|
|
544
|
+
emit.bullet(`Run ${emit.code(`vana connect ${source}`)} without ${emit.code("--no-input")}.`);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
emit.bullet(`Complete the browser step locally, then rerun ${emit.code(`vana connect ${source}`)}.`);
|
|
548
|
+
}
|
|
549
|
+
if (event.logPath || fetchLogPath) {
|
|
550
|
+
emit.bullet(`Inspect the latest run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
551
|
+
}
|
|
552
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
553
|
+
if (event.logPath) {
|
|
554
|
+
emit.keyValue("Run log", formatDisplayPath(event.logPath), "muted");
|
|
555
|
+
}
|
|
556
|
+
emit.event({
|
|
557
|
+
type: "outcome",
|
|
558
|
+
status: CliOutcomeStatus.LEGACY_AUTH,
|
|
559
|
+
source: resolution.source,
|
|
560
|
+
});
|
|
561
|
+
terminalExitCode = 1;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (event.type === "collection-complete" && event.resultPath) {
|
|
565
|
+
collectedResult = true;
|
|
566
|
+
resultPath = event.resultPath;
|
|
567
|
+
const ingestEvents = await ingestResult(resolution.source, resultPath, target);
|
|
568
|
+
for (const ingestEvent of ingestEvents) {
|
|
569
|
+
emit.event(ingestEvent);
|
|
570
|
+
}
|
|
571
|
+
const ingestCompleted = ingestEvents.some((ingestEvent) => ingestEvent.type === "ingest-complete");
|
|
572
|
+
const ingestFailedEvent = ingestEvents.find((ingestEvent) => ingestEvent.type === "ingest-failed");
|
|
573
|
+
if (ingestCompleted) {
|
|
574
|
+
finalStatus = CliOutcomeStatus.CONNECTED_AND_INGESTED;
|
|
575
|
+
finalDataState = "ingested_personal_server";
|
|
576
|
+
}
|
|
577
|
+
else if (ingestFailedEvent?.type === "ingest-failed") {
|
|
578
|
+
finalStatus = CliOutcomeStatus.INGEST_FAILED;
|
|
579
|
+
finalDataState = "ingest_failed";
|
|
580
|
+
ingestFailureMessage =
|
|
581
|
+
ingestFailedEvent.message ?? "Personal Server sync failed.";
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
finalStatus = CliOutcomeStatus.CONNECTED_LOCAL_ONLY;
|
|
585
|
+
finalDataState = "collected_local";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (terminalExitCode !== null) {
|
|
590
|
+
return terminalExitCode;
|
|
591
|
+
}
|
|
592
|
+
if (!collectedResult) {
|
|
593
|
+
await updateSourceState(resolution.source, {
|
|
594
|
+
connectorInstalled: true,
|
|
595
|
+
sessionPresent: fs.existsSync(profilePath),
|
|
596
|
+
lastRunAt: new Date().toISOString(),
|
|
597
|
+
lastRunOutcome: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
|
|
598
|
+
dataState: "none",
|
|
599
|
+
lastError: "Connector run ended without a result.",
|
|
600
|
+
lastResultPath: null,
|
|
601
|
+
lastLogPath: runLogPath ?? fetchLogPath ?? null,
|
|
602
|
+
});
|
|
603
|
+
emit.event({
|
|
604
|
+
type: "outcome",
|
|
605
|
+
status: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
|
|
606
|
+
source: resolution.source,
|
|
607
|
+
reason: "Connector run ended without a result.",
|
|
608
|
+
});
|
|
609
|
+
if (runLogPath) {
|
|
610
|
+
emit.info(`Run log: ${formatDisplayPath(runLogPath)}`);
|
|
611
|
+
}
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
await updateSourceState(resolution.source, {
|
|
615
|
+
connectorInstalled: true,
|
|
616
|
+
sessionPresent: true,
|
|
617
|
+
lastRunAt: new Date().toISOString(),
|
|
618
|
+
lastRunOutcome: finalStatus,
|
|
619
|
+
dataState: finalDataState,
|
|
620
|
+
lastError: ingestFailureMessage,
|
|
621
|
+
lastResultPath: resultPath,
|
|
622
|
+
lastLogPath: runLogPath ?? fetchLogPath ?? setupLogPath ?? null,
|
|
623
|
+
});
|
|
624
|
+
const resultSummary = await readResultSummary(resultPath);
|
|
625
|
+
const statusCommand = emit.code("vana status");
|
|
626
|
+
const dataCommand = emit.code(`vana data show ${source}`);
|
|
627
|
+
const successSummary = finalStatus === CliOutcomeStatus.CONNECTED_AND_INGESTED
|
|
628
|
+
? `Collected your ${displayName} data and synced it to your Personal Server.`
|
|
629
|
+
: `Collected your ${displayName} data and saved it locally.`;
|
|
630
|
+
emit.success(`Connected ${displayName}.`);
|
|
631
|
+
progress.succeed(`Connected ${displayName}.`);
|
|
632
|
+
emit.detail(successSummary);
|
|
633
|
+
emit.blank();
|
|
634
|
+
if (resultSummary) {
|
|
635
|
+
emit.section("Collected");
|
|
636
|
+
for (const line of resultSummary.lines) {
|
|
637
|
+
emit.bullet(line);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
emit.blank();
|
|
641
|
+
emit.section(finalStatus === CliOutcomeStatus.CONNECTED_AND_INGESTED
|
|
642
|
+
? "Saved and synced"
|
|
643
|
+
: "Saved locally");
|
|
644
|
+
emit.keyValue("Path", formatDisplayPath(resultPath), "muted");
|
|
645
|
+
emit.keyValue("Session", "Saved for faster reconnects.", "muted");
|
|
646
|
+
if (finalStatus === CliOutcomeStatus.CONNECTED_AND_INGESTED) {
|
|
647
|
+
emit.keyValue("Server", "Your data is now available in your Personal Server.", "success");
|
|
648
|
+
}
|
|
649
|
+
else if (finalStatus === CliOutcomeStatus.INGEST_FAILED &&
|
|
650
|
+
ingestFailureMessage) {
|
|
651
|
+
emit.keyValue("Server", `Sync failed: ${ingestFailureMessage}`, "warning");
|
|
652
|
+
}
|
|
653
|
+
else if (target.state !== "available") {
|
|
654
|
+
emit.keyValue("Server", "Unavailable, so this run stayed local.", "muted");
|
|
655
|
+
}
|
|
656
|
+
if (runLogPath) {
|
|
657
|
+
emit.keyValue("Run log", formatDisplayPath(runLogPath), "muted");
|
|
658
|
+
}
|
|
659
|
+
else if (fetchLogPath) {
|
|
660
|
+
emit.keyValue("Fetch log", formatDisplayPath(fetchLogPath), "muted");
|
|
661
|
+
}
|
|
662
|
+
else if (setupLogPath) {
|
|
663
|
+
emit.keyValue("Setup log", formatDisplayPath(setupLogPath), "muted");
|
|
664
|
+
}
|
|
665
|
+
emit.blank();
|
|
666
|
+
emit.section("Next");
|
|
667
|
+
emit.bullet(`Inspect the data with ${dataCommand}`);
|
|
668
|
+
emit.bullet(`Connect another source with ${emit.code("vana sources")}`);
|
|
669
|
+
if (runLogPath) {
|
|
670
|
+
emit.bullet(`Inspect the run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
671
|
+
}
|
|
672
|
+
emit.bullet(`Or check overall status with ${statusCommand}`);
|
|
673
|
+
emit.event({
|
|
674
|
+
type: "outcome",
|
|
675
|
+
status: finalStatus,
|
|
676
|
+
source: resolution.source,
|
|
677
|
+
resultPath,
|
|
678
|
+
});
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
if (error instanceof Error &&
|
|
683
|
+
error.message === "__vana_prompt_cancelled__") {
|
|
684
|
+
await updateSourceState(source, {
|
|
685
|
+
lastRunAt: new Date().toISOString(),
|
|
686
|
+
lastRunOutcome: CliOutcomeStatus.NEEDS_INPUT,
|
|
687
|
+
lastError: "Cancelled before input was completed.",
|
|
688
|
+
lastLogPath: runLogPath ?? null,
|
|
689
|
+
});
|
|
690
|
+
emit.blank();
|
|
691
|
+
progress.stop();
|
|
692
|
+
emit.section("Cancelled");
|
|
693
|
+
emit.info(`Stopped before ${displayName} finished collecting your data.`);
|
|
694
|
+
emit.detail("No credentials were sent anywhere.");
|
|
695
|
+
emit.blank();
|
|
696
|
+
emit.section("Next");
|
|
697
|
+
emit.bullet(`Resume with ${emit.code(`vana connect ${source}`)}.`);
|
|
698
|
+
if (runLogPath) {
|
|
699
|
+
emit.bullet(`Inspect the latest run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
700
|
+
}
|
|
701
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
702
|
+
emit.event({
|
|
703
|
+
type: "outcome",
|
|
704
|
+
status: CliOutcomeStatus.NEEDS_INPUT,
|
|
705
|
+
source,
|
|
706
|
+
reason: "prompt_cancelled",
|
|
707
|
+
});
|
|
708
|
+
if (runLogPath) {
|
|
709
|
+
emit.keyValue("Run log", formatDisplayPath(runLogPath), "muted");
|
|
710
|
+
}
|
|
711
|
+
return 1;
|
|
712
|
+
}
|
|
713
|
+
const message = error instanceof Error ? error.message : "Unexpected error.";
|
|
714
|
+
progress.fail(`Problem connecting ${displayName}.`);
|
|
715
|
+
emit.info(message);
|
|
716
|
+
emit.event({
|
|
717
|
+
type: "outcome",
|
|
718
|
+
status: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
|
|
719
|
+
source,
|
|
720
|
+
reason: message,
|
|
721
|
+
});
|
|
722
|
+
emit.blank();
|
|
723
|
+
emit.section("Next");
|
|
724
|
+
if (runLogPath || fetchLogPath || setupLogPath) {
|
|
725
|
+
emit.bullet(`Inspect the latest run log with ${emit.code(`vana logs ${source}`)}.`);
|
|
726
|
+
}
|
|
727
|
+
emit.bullet(`Inspect install health with ${emit.code("vana doctor")}.`);
|
|
728
|
+
emit.bullet(`Or check overall status with ${emit.code("vana status")}.`);
|
|
729
|
+
if (runLogPath) {
|
|
730
|
+
emit.detail(`Run log: ${formatDisplayPath(runLogPath)}`);
|
|
731
|
+
}
|
|
732
|
+
else if (fetchLogPath) {
|
|
733
|
+
emit.detail(`Fetch log: ${formatDisplayPath(fetchLogPath)}`);
|
|
734
|
+
}
|
|
735
|
+
else if (setupLogPath) {
|
|
736
|
+
emit.detail(`Setup log: ${formatDisplayPath(setupLogPath)}`);
|
|
737
|
+
}
|
|
738
|
+
return 1;
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
progress.stop();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
async function runConnectEntry(options) {
|
|
745
|
+
const emit = createEmitter(options);
|
|
746
|
+
const sources = await loadRegistrySources();
|
|
747
|
+
const state = await readCliState();
|
|
748
|
+
const sourceMetadata = createSourceMetadataMap(sources);
|
|
749
|
+
const statuses = await gatherSourceStatuses(state.sources, sourceMetadata);
|
|
750
|
+
const statusMap = new Map(statuses.map((source) => [source.source, source]));
|
|
751
|
+
const enrichedSources = sources.map((source) => {
|
|
752
|
+
const status = statusMap.get(source.id);
|
|
753
|
+
return {
|
|
754
|
+
...source,
|
|
755
|
+
dataState: status?.dataState,
|
|
756
|
+
lastRunOutcome: status?.lastRunOutcome ?? null,
|
|
757
|
+
sessionPresent: status?.sessionPresent ?? false,
|
|
758
|
+
};
|
|
759
|
+
});
|
|
760
|
+
const suggestedSource = enrichedSources.find((source) => source.authMode !== "legacy" && !hasCollectedData(source.dataState)) ??
|
|
761
|
+
enrichedSources.find((source) => source.authMode !== "legacy") ??
|
|
762
|
+
enrichedSources[0];
|
|
763
|
+
const missingSourceMessage = formatMissingConnectSourceMessage(suggestedSource);
|
|
764
|
+
if (options.json) {
|
|
765
|
+
process.stdout.write(`${JSON.stringify({
|
|
766
|
+
error: "source_required",
|
|
767
|
+
message: missingSourceMessage,
|
|
768
|
+
suggestedSource: suggestedSource
|
|
769
|
+
? {
|
|
770
|
+
id: suggestedSource.id,
|
|
771
|
+
name: suggestedSource.name,
|
|
772
|
+
authMode: suggestedSource.authMode,
|
|
773
|
+
}
|
|
774
|
+
: null,
|
|
775
|
+
})}\n`);
|
|
776
|
+
return 1;
|
|
777
|
+
}
|
|
778
|
+
if (options.noInput) {
|
|
779
|
+
emit.info(missingSourceMessage);
|
|
780
|
+
return 1;
|
|
781
|
+
}
|
|
782
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
783
|
+
emit.info(missingSourceMessage);
|
|
784
|
+
return 1;
|
|
785
|
+
}
|
|
786
|
+
if (enrichedSources.length === 0) {
|
|
787
|
+
emit.info("No sources are available right now.");
|
|
788
|
+
emit.info("Run `vana sources` to verify the local connector registry.");
|
|
789
|
+
return 1;
|
|
790
|
+
}
|
|
791
|
+
emit.title("Connect data");
|
|
792
|
+
emit.blank();
|
|
793
|
+
const connectedCount = enrichedSources.filter((source) => hasCollectedData(source.dataState)).length;
|
|
794
|
+
const readyNowCount = enrichedSources.filter((source) => source.authMode !== "legacy" && !hasCollectedData(source.dataState)).length;
|
|
795
|
+
const manualCount = enrichedSources.filter((source) => source.authMode === "legacy" && !hasCollectedData(source.dataState)).length;
|
|
796
|
+
if (connectedCount > 0 || readyNowCount > 0 || manualCount > 0) {
|
|
797
|
+
const parts = [];
|
|
798
|
+
if (connectedCount > 0) {
|
|
799
|
+
parts.push(`${connectedCount} connected ${connectedCount === 1 ? "source" : "sources"}`);
|
|
800
|
+
}
|
|
801
|
+
if (readyNowCount > 0) {
|
|
802
|
+
parts.push(`${readyNowCount} ready ${readyNowCount === 1 ? "source" : "sources"}`);
|
|
803
|
+
}
|
|
804
|
+
if (manualCount > 0) {
|
|
805
|
+
parts.push(`${manualCount} with manual ${manualCount === 1 ? "step" : "steps"}`);
|
|
806
|
+
}
|
|
807
|
+
emit.detail(parts.join(" • "));
|
|
808
|
+
}
|
|
809
|
+
emit.info("Choose a source to connect:");
|
|
810
|
+
if (connectedCount > 0) {
|
|
811
|
+
emit.detail(`Inspect what you already collected with ${emit.code("vana data list")}, or reconnect any source below.`);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
emit.detail(`Or jump straight in with ${emit.code("vana connect <source>")}.`);
|
|
815
|
+
}
|
|
816
|
+
let source;
|
|
817
|
+
try {
|
|
818
|
+
source = await select({
|
|
819
|
+
message: "Source",
|
|
820
|
+
pageSize: 8,
|
|
821
|
+
choices: buildConnectChoices(enrichedSources, emit, suggestedSource?.id ?? null),
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
if (isPromptCancelled(error)) {
|
|
826
|
+
emit.info("Cancelled. No source was connected.");
|
|
827
|
+
emit.detail(`Browse sources any time with ${emit.code("vana sources")}.`);
|
|
828
|
+
return 1;
|
|
829
|
+
}
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
emit.blank();
|
|
833
|
+
return runConnect(source, options);
|
|
834
|
+
}
|
|
835
|
+
async function runList(options) {
|
|
836
|
+
const sources = await loadRegistrySources();
|
|
837
|
+
const state = await readCliState();
|
|
838
|
+
const sourceMetadata = createSourceMetadataMap(sources);
|
|
839
|
+
const statuses = await gatherSourceStatuses(state.sources, sourceMetadata);
|
|
840
|
+
const statusMap = new Map(statuses.map((source) => [source.source, source]));
|
|
841
|
+
const installedSourceIds = new Set((await listInstalledConnectorFiles()).map((source) => source.source));
|
|
842
|
+
const enrichedSources = sources.map((source) => {
|
|
843
|
+
const status = statusMap.get(source.id);
|
|
844
|
+
return {
|
|
845
|
+
...source,
|
|
846
|
+
installed: installedSourceIds.has(source.id),
|
|
847
|
+
dataState: status?.dataState,
|
|
848
|
+
lastRunOutcome: status?.lastRunOutcome ?? null,
|
|
849
|
+
sessionPresent: status?.sessionPresent ?? false,
|
|
850
|
+
};
|
|
851
|
+
});
|
|
852
|
+
const readyCount = enrichedSources.filter((source) => source.authMode !== "legacy" && !hasCollectedData(source.dataState)).length;
|
|
853
|
+
const manualCount = enrichedSources.filter((source) => source.authMode === "legacy" && !hasCollectedData(source.dataState)).length;
|
|
854
|
+
const connectedCount = enrichedSources.filter((source) => source.dataState === "collected_local" ||
|
|
855
|
+
source.dataState === "ingested_personal_server" ||
|
|
856
|
+
source.dataState === "ingest_failed").length;
|
|
857
|
+
const recommendedSource = enrichedSources.find((source) => source.authMode !== "legacy" &&
|
|
858
|
+
source.dataState !== "collected_local" &&
|
|
859
|
+
source.dataState !== "ingested_personal_server" &&
|
|
860
|
+
source.dataState !== "ingest_failed") ??
|
|
861
|
+
enrichedSources.find((source) => source.dataState !== "collected_local" &&
|
|
862
|
+
source.dataState !== "ingested_personal_server" &&
|
|
863
|
+
source.dataState !== "ingest_failed") ??
|
|
864
|
+
null;
|
|
865
|
+
const nextSteps = buildSourcesNextSteps(recommendedSource, connectedCount);
|
|
866
|
+
if (options.json) {
|
|
867
|
+
process.stdout.write(`${JSON.stringify({
|
|
868
|
+
count: enrichedSources.length,
|
|
869
|
+
recommendedSource,
|
|
870
|
+
nextSteps,
|
|
871
|
+
summary: {
|
|
872
|
+
connectedCount,
|
|
873
|
+
readyCount,
|
|
874
|
+
manualCount,
|
|
875
|
+
installedCount: enrichedSources.filter((source) => source.installed)
|
|
876
|
+
.length,
|
|
877
|
+
},
|
|
878
|
+
sources: enrichedSources,
|
|
879
|
+
})}\n`);
|
|
880
|
+
return 0;
|
|
881
|
+
}
|
|
882
|
+
const emit = createEmitter(options);
|
|
883
|
+
emit.title(enrichedSources.length > 0
|
|
884
|
+
? `Available sources (${enrichedSources.length})`
|
|
885
|
+
: "Available sources");
|
|
886
|
+
emit.blank();
|
|
887
|
+
if (enrichedSources.length > 0) {
|
|
888
|
+
emit.info(joinOverviewParts([
|
|
889
|
+
connectedCount > 0 ? formatCountLabel("connected", connectedCount) : "",
|
|
890
|
+
readyCount > 0 ? formatCountLabel("ready now", readyCount) : "",
|
|
891
|
+
manualCount > 0
|
|
892
|
+
? formatCountLabel("with manual step", manualCount)
|
|
893
|
+
: "",
|
|
894
|
+
]));
|
|
895
|
+
emit.blank();
|
|
896
|
+
}
|
|
897
|
+
const groups = [
|
|
898
|
+
{
|
|
899
|
+
title: "Connected",
|
|
900
|
+
items: enrichedSources.filter((source) => source.dataState === "collected_local" ||
|
|
901
|
+
source.dataState === "ingested_personal_server" ||
|
|
902
|
+
source.dataState === "ingest_failed"),
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
title: "Ready now",
|
|
906
|
+
items: enrichedSources.filter((source) => source.authMode !== "legacy" &&
|
|
907
|
+
source.dataState !== "collected_local" &&
|
|
908
|
+
source.dataState !== "ingested_personal_server" &&
|
|
909
|
+
source.dataState !== "ingest_failed"),
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
title: "Manual steps",
|
|
913
|
+
items: enrichedSources.filter((source) => source.authMode === "legacy" &&
|
|
914
|
+
source.dataState !== "collected_local" &&
|
|
915
|
+
source.dataState !== "ingested_personal_server" &&
|
|
916
|
+
source.dataState !== "ingest_failed"),
|
|
917
|
+
},
|
|
918
|
+
].filter((group) => group.items.length > 0);
|
|
919
|
+
groups.forEach((group, index) => {
|
|
920
|
+
if (index > 0) {
|
|
921
|
+
emit.blank();
|
|
922
|
+
}
|
|
923
|
+
emit.section(formatCountLabel(group.title, group.items.length));
|
|
924
|
+
for (const source of group.items) {
|
|
925
|
+
const badges = [];
|
|
926
|
+
if (source.dataState === "ingested_personal_server" ||
|
|
927
|
+
source.dataState === "collected_local" ||
|
|
928
|
+
source.dataState === "ingest_failed") {
|
|
929
|
+
if (source.dataState === "ingested_personal_server") {
|
|
930
|
+
badges.push({ text: "synced", tone: "success" });
|
|
931
|
+
}
|
|
932
|
+
else if (source.dataState === "ingest_failed") {
|
|
933
|
+
badges.push({ text: "sync failed", tone: "warning" });
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
badges.push({ text: "local", tone: "muted" });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (source.authMode === "interactive") {
|
|
940
|
+
badges.push({ text: "interactive", tone: "info" });
|
|
941
|
+
}
|
|
942
|
+
else if (source.authMode === "legacy") {
|
|
943
|
+
badges.push({ text: "legacy", tone: "warning" });
|
|
944
|
+
}
|
|
945
|
+
if (recommendedSource?.id === source.id &&
|
|
946
|
+
recommendedSource.authMode !== "legacy") {
|
|
947
|
+
badges.push({ text: "recommended", tone: "accent" });
|
|
948
|
+
}
|
|
949
|
+
if (source.installed) {
|
|
950
|
+
badges.push({ text: "installed", tone: "success" });
|
|
951
|
+
}
|
|
952
|
+
emit.sourceTitle(source.name, badges);
|
|
953
|
+
if (source.description) {
|
|
954
|
+
emit.detail(source.description);
|
|
955
|
+
}
|
|
956
|
+
if (source.dataState === "collected_local" ||
|
|
957
|
+
source.dataState === "ingested_personal_server" ||
|
|
958
|
+
source.dataState === "ingest_failed") {
|
|
959
|
+
emit.detail(`Inspect with ${emit.code(`vana data show ${source.id}`)}.`);
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
emit.detail(describeSourceFlow(source.authMode));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
if (groups.length === 0) {
|
|
967
|
+
emit.info("No sources are available right now.");
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
if (nextSteps.length > 0) {
|
|
971
|
+
emit.blank();
|
|
972
|
+
emit.section("Next");
|
|
973
|
+
for (const step of nextSteps) {
|
|
974
|
+
emit.bullet(step);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return 0;
|
|
979
|
+
}
|
|
980
|
+
async function runStatus(options) {
|
|
981
|
+
const emit = createEmitter(options);
|
|
982
|
+
const runtime = new ManagedPlaywrightRuntime();
|
|
983
|
+
const personalServer = await detectPersonalServerTarget();
|
|
984
|
+
const state = await readCliState();
|
|
985
|
+
const registrySources = await loadRegistrySources();
|
|
986
|
+
const sourceLabels = createSourceLabelMap(registrySources);
|
|
987
|
+
const sourceMetadata = createSourceMetadataMap(registrySources);
|
|
988
|
+
const sources = await gatherSourceStatuses(state.sources, sourceMetadata);
|
|
989
|
+
const status = {
|
|
990
|
+
cliVersion: getCliVersion(),
|
|
991
|
+
channel: getCliChannel(),
|
|
992
|
+
installMethod: getCliInstallMethod(),
|
|
993
|
+
runtime: runtime.state,
|
|
994
|
+
runtimePath: runtime.runtimePath,
|
|
995
|
+
personalServer: personalServer.state,
|
|
996
|
+
personalServerUrl: personalServer.url,
|
|
997
|
+
summary: {
|
|
998
|
+
sourceCount: sources.length,
|
|
999
|
+
needsAttentionCount: sources.filter((source) => rankSourceStatus(source) <= 4).length,
|
|
1000
|
+
connectedCount: sources.filter((source) => source.dataState === "ingested_personal_server" ||
|
|
1001
|
+
source.dataState === "collected_local" ||
|
|
1002
|
+
source.dataState === "ingest_failed").length,
|
|
1003
|
+
installedCount: sources.filter((source) => source.installed).length,
|
|
1004
|
+
localCount: sources.filter((source) => source.dataState === "collected_local").length,
|
|
1005
|
+
syncedCount: sources.filter((source) => source.dataState === "ingested_personal_server").length,
|
|
1006
|
+
syncFailedCount: sources.filter((source) => source.dataState === "ingest_failed").length,
|
|
1007
|
+
},
|
|
1008
|
+
sources,
|
|
1009
|
+
};
|
|
1010
|
+
const nextSteps = buildStatusNextSteps(status.sources, sourceLabels, status.runtime, registrySources);
|
|
1011
|
+
if (options.json) {
|
|
1012
|
+
process.stdout.write(`${JSON.stringify({ ...status, nextSteps })}\n`);
|
|
1013
|
+
return 0;
|
|
1014
|
+
}
|
|
1015
|
+
emit.title("Vana Connect status");
|
|
1016
|
+
emit.blank();
|
|
1017
|
+
emit.info(joinOverviewParts([
|
|
1018
|
+
(status.summary?.needsAttentionCount ?? 0) > 0
|
|
1019
|
+
? formatCountLabel("need attention", status.summary?.needsAttentionCount ?? 0)
|
|
1020
|
+
: "",
|
|
1021
|
+
(status.summary?.connectedCount ?? 0) > 0
|
|
1022
|
+
? formatCountLabel("connected", status.summary?.connectedCount ?? 0)
|
|
1023
|
+
: "",
|
|
1024
|
+
(status.summary?.localCount ?? 0) > 0
|
|
1025
|
+
? formatCountLabel("local only", status.summary?.localCount ?? 0)
|
|
1026
|
+
: "",
|
|
1027
|
+
(status.summary?.syncedCount ?? 0) > 0
|
|
1028
|
+
? formatCountLabel("synced", status.summary?.syncedCount ?? 0)
|
|
1029
|
+
: "",
|
|
1030
|
+
(status.summary?.syncFailedCount ?? 0) > 0
|
|
1031
|
+
? formatCountLabel("sync failed", status.summary?.syncFailedCount ?? 0)
|
|
1032
|
+
: "",
|
|
1033
|
+
(status.summary?.connectedCount ?? 0) === 0 &&
|
|
1034
|
+
(status.summary?.installedCount ?? 0) > 0
|
|
1035
|
+
? formatCountLabel("installed", status.summary?.installedCount ?? 0)
|
|
1036
|
+
: "",
|
|
1037
|
+
]));
|
|
1038
|
+
emit.blank();
|
|
1039
|
+
emit.section("Environment");
|
|
1040
|
+
emit.keyValue("Runtime", status.runtime, toneForRuntime(status.runtime));
|
|
1041
|
+
if (status.runtimePath) {
|
|
1042
|
+
emit.keyValue("Browser", formatDisplayPath(status.runtimePath), "muted");
|
|
1043
|
+
}
|
|
1044
|
+
emit.keyValue("Personal Server", status.personalServer, status.personalServer === "available" ? "success" : "muted");
|
|
1045
|
+
if (status.personalServerUrl) {
|
|
1046
|
+
emit.detail(status.personalServerUrl);
|
|
1047
|
+
}
|
|
1048
|
+
const sourceGroups = [
|
|
1049
|
+
{
|
|
1050
|
+
title: "Needs attention",
|
|
1051
|
+
items: status.sources.filter((source) => rankSourceStatus(source) <= 4),
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
title: "Connected",
|
|
1055
|
+
items: status.sources.filter((source) => source.dataState === "ingested_personal_server" ||
|
|
1056
|
+
source.dataState === "collected_local" ||
|
|
1057
|
+
source.dataState === "ingest_failed"),
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
title: "Installed",
|
|
1061
|
+
items: status.sources.filter((source) => rankSourceStatus(source) > 4 &&
|
|
1062
|
+
source.dataState === "none" &&
|
|
1063
|
+
source.installed),
|
|
1064
|
+
},
|
|
1065
|
+
].filter((group) => group.items.length > 0);
|
|
1066
|
+
if (sourceGroups.length > 0) {
|
|
1067
|
+
emit.blank();
|
|
1068
|
+
}
|
|
1069
|
+
sourceGroups.forEach((group, index) => {
|
|
1070
|
+
if (index > 0) {
|
|
1071
|
+
emit.blank();
|
|
1072
|
+
}
|
|
1073
|
+
emit.section(formatCountLabel(group.title, group.items.length));
|
|
1074
|
+
for (const source of group.items) {
|
|
1075
|
+
const status = getSourceStatusPresentation(source);
|
|
1076
|
+
const badges = [];
|
|
1077
|
+
if (source.authMode === "interactive") {
|
|
1078
|
+
badges.push({ text: "interactive", tone: "info" });
|
|
1079
|
+
}
|
|
1080
|
+
else if (source.authMode === "legacy") {
|
|
1081
|
+
badges.push({ text: "legacy", tone: "warning" });
|
|
1082
|
+
}
|
|
1083
|
+
badges.push({ text: status.label, tone: status.tone });
|
|
1084
|
+
emit.sourceTitle(displaySource(source.source, sourceLabels), badges);
|
|
1085
|
+
const details = formatSourceStatusDetails(source);
|
|
1086
|
+
for (const detail of details) {
|
|
1087
|
+
if (detail.kind === "row") {
|
|
1088
|
+
emit.keyValue(detail.label, detail.value, detail.tone ?? "muted");
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
emit.detail(detail.message);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
if (nextSteps.length > 0) {
|
|
1096
|
+
emit.blank();
|
|
1097
|
+
emit.section("Next");
|
|
1098
|
+
for (const step of nextSteps) {
|
|
1099
|
+
emit.bullet(step);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return 0;
|
|
1103
|
+
}
|
|
1104
|
+
async function runDoctor(options) {
|
|
1105
|
+
const runtime = new ManagedPlaywrightRuntime();
|
|
1106
|
+
const personalServer = await detectPersonalServerTarget();
|
|
1107
|
+
const state = await readCliState();
|
|
1108
|
+
const registrySources = await loadRegistrySources();
|
|
1109
|
+
const sourceMetadata = createSourceMetadataMap(registrySources);
|
|
1110
|
+
const sourceLabels = createSourceLabelMap(registrySources);
|
|
1111
|
+
const sources = await gatherSourceStatuses(state.sources, sourceMetadata);
|
|
1112
|
+
const cliVersion = getCliVersion();
|
|
1113
|
+
const cliChannel = getCliChannel(cliVersion);
|
|
1114
|
+
const installMethod = getCliInstallMethod();
|
|
1115
|
+
const lifecycle = getLifecycleCommands(installMethod, cliChannel);
|
|
1116
|
+
const appRootPath = getDoctorAppRootPath(installMethod);
|
|
1117
|
+
const recentSources = [...sources]
|
|
1118
|
+
.filter((source) => Boolean(source.lastRunAt))
|
|
1119
|
+
.sort(compareSourceStatusOrder)
|
|
1120
|
+
.slice(0, 3);
|
|
1121
|
+
const attentionSources = recentSources.filter((source) => rankSourceStatus(source) <= 4);
|
|
1122
|
+
const connectedCount = sources.filter((source) => source.dataState === "collected_local" ||
|
|
1123
|
+
source.dataState === "ingested_personal_server" ||
|
|
1124
|
+
source.dataState === "ingest_failed").length;
|
|
1125
|
+
const attentionCount = sources.filter((source) => rankSourceStatus(source) <= 4).length;
|
|
1126
|
+
const directories = [
|
|
1127
|
+
{
|
|
1128
|
+
key: "executable",
|
|
1129
|
+
label: "Executable",
|
|
1130
|
+
path: process.execPath,
|
|
1131
|
+
present: fs.existsSync(process.execPath),
|
|
1132
|
+
},
|
|
1133
|
+
...(appRootPath
|
|
1134
|
+
? [
|
|
1135
|
+
{
|
|
1136
|
+
key: "appRoot",
|
|
1137
|
+
label: "App root",
|
|
1138
|
+
path: appRootPath,
|
|
1139
|
+
present: fs.existsSync(appRootPath),
|
|
1140
|
+
},
|
|
1141
|
+
]
|
|
1142
|
+
: []),
|
|
1143
|
+
{
|
|
1144
|
+
key: "dataHome",
|
|
1145
|
+
label: "Data home",
|
|
1146
|
+
path: getDataConnectHome(),
|
|
1147
|
+
present: fs.existsSync(getDataConnectHome()),
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
key: "stateFile",
|
|
1151
|
+
label: "State file",
|
|
1152
|
+
path: getCliStatePath(),
|
|
1153
|
+
present: fs.existsSync(getCliStatePath()),
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
key: "connectorCache",
|
|
1157
|
+
label: "Connector cache",
|
|
1158
|
+
path: getConnectorCacheDir(),
|
|
1159
|
+
present: fs.existsSync(getConnectorCacheDir()),
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
key: "browserProfiles",
|
|
1163
|
+
label: "Browser profiles",
|
|
1164
|
+
path: getBrowserProfilesDir(),
|
|
1165
|
+
present: fs.existsSync(getBrowserProfilesDir()),
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
key: "logs",
|
|
1169
|
+
label: "Logs",
|
|
1170
|
+
path: getLogsDir(),
|
|
1171
|
+
present: fs.existsSync(getLogsDir()),
|
|
1172
|
+
},
|
|
1173
|
+
];
|
|
1174
|
+
const checks = [
|
|
1175
|
+
{
|
|
1176
|
+
key: "cli",
|
|
1177
|
+
label: "CLI",
|
|
1178
|
+
status: "ok",
|
|
1179
|
+
detail: `Version ${cliVersion}`,
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
key: "runtime",
|
|
1183
|
+
label: "Runtime",
|
|
1184
|
+
status: runtime.state === "installed" ? "ok" : "warn",
|
|
1185
|
+
detail: runtime.state === "installed"
|
|
1186
|
+
? `Browser available at ${formatDisplayPath(runtime.runtimePath ?? "unknown")}`
|
|
1187
|
+
: "Run `vana setup` to install the local browser runtime.",
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
key: "personalServer",
|
|
1191
|
+
label: "Personal Server",
|
|
1192
|
+
status: personalServer.state === "available" ? "ok" : "warn",
|
|
1193
|
+
detail: personalServer.state === "available"
|
|
1194
|
+
? (personalServer.url ?? "Available")
|
|
1195
|
+
: "Unavailable. Connects will stay local until a Personal Server is reachable.",
|
|
1196
|
+
},
|
|
1197
|
+
...directories.map((entry) => ({
|
|
1198
|
+
key: entry.key,
|
|
1199
|
+
label: entry.label,
|
|
1200
|
+
status: entry.present ? "ok" : "warn",
|
|
1201
|
+
detail: `${entry.present ? "Present" : "Missing"} at ${formatDisplayPath(entry.path)}`,
|
|
1202
|
+
})),
|
|
1203
|
+
{
|
|
1204
|
+
key: "sources",
|
|
1205
|
+
label: "Tracked sources",
|
|
1206
|
+
status: "ok",
|
|
1207
|
+
detail: `${Object.keys(state.sources).length} source${Object.keys(state.sources).length === 1 ? "" : "s"} in local state`,
|
|
1208
|
+
},
|
|
1209
|
+
...(attentionSources[0]
|
|
1210
|
+
? [
|
|
1211
|
+
{
|
|
1212
|
+
key: "latestIssue",
|
|
1213
|
+
label: "Latest issue",
|
|
1214
|
+
status: "warn",
|
|
1215
|
+
detail: `${displaySource(attentionSources[0].source, sourceLabels)}: ${attentionSources[0].lastError ?? getSourceStatusPresentation(attentionSources[0]).label}`,
|
|
1216
|
+
},
|
|
1217
|
+
]
|
|
1218
|
+
: []),
|
|
1219
|
+
];
|
|
1220
|
+
const nextSteps = [
|
|
1221
|
+
...(runtime.state !== "installed"
|
|
1222
|
+
? ["Install the local runtime with `vana setup`."]
|
|
1223
|
+
: []),
|
|
1224
|
+
...(personalServer.state !== "available"
|
|
1225
|
+
? [
|
|
1226
|
+
"Your Personal Server is unavailable, so successful runs will stay local.",
|
|
1227
|
+
]
|
|
1228
|
+
: []),
|
|
1229
|
+
...(Object.keys(state.sources).length === 0
|
|
1230
|
+
? ["Connect your first source with `vana connect`."]
|
|
1231
|
+
: ["Check overall status with `vana status`."]),
|
|
1232
|
+
...(attentionSources[0]?.lastLogPath
|
|
1233
|
+
? [
|
|
1234
|
+
`Inspect the latest issue log with \`vana logs ${attentionSources[0].source}\`.`,
|
|
1235
|
+
]
|
|
1236
|
+
: recentSources[0]?.lastLogPath
|
|
1237
|
+
? [
|
|
1238
|
+
`Inspect the latest run log with \`vana logs ${recentSources[0].source}\`.`,
|
|
1239
|
+
]
|
|
1240
|
+
: []),
|
|
1241
|
+
];
|
|
1242
|
+
const payload = {
|
|
1243
|
+
cliVersion,
|
|
1244
|
+
channel: cliChannel,
|
|
1245
|
+
installMethod,
|
|
1246
|
+
runtime: runtime.state,
|
|
1247
|
+
runtimePath: runtime.runtimePath,
|
|
1248
|
+
personalServer: personalServer.state,
|
|
1249
|
+
personalServerUrl: personalServer.url,
|
|
1250
|
+
capabilities: runtime.capabilities,
|
|
1251
|
+
paths: {
|
|
1252
|
+
executable: process.execPath,
|
|
1253
|
+
appRoot: appRootPath,
|
|
1254
|
+
dataHome: getDataConnectHome(),
|
|
1255
|
+
stateFile: getCliStatePath(),
|
|
1256
|
+
connectorCache: getConnectorCacheDir(),
|
|
1257
|
+
browserProfiles: getBrowserProfilesDir(),
|
|
1258
|
+
logs: getLogsDir(),
|
|
1259
|
+
},
|
|
1260
|
+
lifecycle,
|
|
1261
|
+
summary: {
|
|
1262
|
+
trackedSourceCount: Object.keys(state.sources).length,
|
|
1263
|
+
attentionCount,
|
|
1264
|
+
connectedCount,
|
|
1265
|
+
},
|
|
1266
|
+
recentSources,
|
|
1267
|
+
checks,
|
|
1268
|
+
nextSteps,
|
|
1269
|
+
};
|
|
1270
|
+
if (options.json) {
|
|
1271
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1272
|
+
return 0;
|
|
1273
|
+
}
|
|
1274
|
+
const emit = createEmitter(options);
|
|
1275
|
+
emit.title("Vana Connect doctor");
|
|
1276
|
+
emit.blank();
|
|
1277
|
+
emit.section("Summary");
|
|
1278
|
+
emit.keyValue("CLI", cliVersion, "muted");
|
|
1279
|
+
emit.keyValue("Channel", cliChannel, "muted");
|
|
1280
|
+
emit.keyValue("Install", formatInstallMethodLabel(installMethod), "muted");
|
|
1281
|
+
emit.keyValue("Runtime", runtime.state, toneForRuntime(runtime.state));
|
|
1282
|
+
emit.keyValue("Personal Server", personalServer.state, personalServer.state === "available" ? "success" : "warning");
|
|
1283
|
+
emit.keyValue("Tracked sources", String(Object.keys(state.sources).length), "muted");
|
|
1284
|
+
emit.keyValue("Attention", String(attentionCount), attentionCount > 0 ? "warning" : "muted");
|
|
1285
|
+
emit.keyValue("Connected", String(connectedCount), connectedCount > 0 ? "success" : "muted");
|
|
1286
|
+
emit.keyValue("Headed sessions", runtime.capabilities.supportsHeaded ? "Available" : "Unavailable", runtime.capabilities.supportsHeaded ? "success" : "warning");
|
|
1287
|
+
emit.keyValue("Managed profiles", runtime.capabilities.supportsManagedProfiles ? "Available" : "Unavailable", runtime.capabilities.supportsManagedProfiles ? "success" : "warning");
|
|
1288
|
+
emit.keyValue("Screenshots", runtime.capabilities.supportsScreenshots ? "Available" : "Unavailable", runtime.capabilities.supportsScreenshots ? "success" : "warning");
|
|
1289
|
+
emit.blank();
|
|
1290
|
+
emit.section("Checks");
|
|
1291
|
+
for (const check of checks) {
|
|
1292
|
+
const tone = check.status === "ok"
|
|
1293
|
+
? "success"
|
|
1294
|
+
: check.status === "warn"
|
|
1295
|
+
? "warning"
|
|
1296
|
+
: "error";
|
|
1297
|
+
emit.keyValue(check.label, check.detail, tone);
|
|
1298
|
+
}
|
|
1299
|
+
if (recentSources.length > 0) {
|
|
1300
|
+
emit.blank();
|
|
1301
|
+
emit.section(attentionSources.length > 0
|
|
1302
|
+
? "Needs attention"
|
|
1303
|
+
: "Recent source activity");
|
|
1304
|
+
for (const source of attentionSources.length > 0
|
|
1305
|
+
? attentionSources
|
|
1306
|
+
: recentSources) {
|
|
1307
|
+
const status = getSourceStatusPresentation(source);
|
|
1308
|
+
const badges = [];
|
|
1309
|
+
if (source.authMode === "interactive") {
|
|
1310
|
+
badges.push({ text: "interactive", tone: "info" });
|
|
1311
|
+
}
|
|
1312
|
+
else if (source.authMode === "legacy") {
|
|
1313
|
+
badges.push({ text: "legacy", tone: "warning" });
|
|
1314
|
+
}
|
|
1315
|
+
badges.push({ text: status.label, tone: status.tone });
|
|
1316
|
+
emit.sourceTitle(displaySource(source.source, sourceLabels), badges);
|
|
1317
|
+
const details = formatSourceStatusDetails(source);
|
|
1318
|
+
for (const detail of details) {
|
|
1319
|
+
if (detail.kind === "row") {
|
|
1320
|
+
emit.keyValue(detail.label, detail.value, detail.tone ?? "muted");
|
|
1321
|
+
}
|
|
1322
|
+
else {
|
|
1323
|
+
emit.detail(detail.message);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
emit.blank();
|
|
1329
|
+
emit.section("Paths");
|
|
1330
|
+
emit.keyValue("Executable", formatDisplayPath(process.execPath), "muted");
|
|
1331
|
+
if (appRootPath) {
|
|
1332
|
+
emit.keyValue("App root", formatDisplayPath(appRootPath), "muted");
|
|
1333
|
+
}
|
|
1334
|
+
emit.keyValue("Data home", formatDisplayPath(getDataConnectHome()), "muted");
|
|
1335
|
+
emit.keyValue("State file", formatDisplayPath(getCliStatePath()), "muted");
|
|
1336
|
+
emit.keyValue("Connector cache", formatDisplayPath(getConnectorCacheDir()), "muted");
|
|
1337
|
+
emit.keyValue("Browser profiles", formatDisplayPath(getBrowserProfilesDir()), "muted");
|
|
1338
|
+
emit.keyValue("Logs", formatDisplayPath(getLogsDir()), "muted");
|
|
1339
|
+
emit.blank();
|
|
1340
|
+
emit.section("Lifecycle");
|
|
1341
|
+
emit.keyValue("Upgrade", lifecycle.upgrade, "muted");
|
|
1342
|
+
emit.keyValue("Uninstall", lifecycle.uninstall, "muted");
|
|
1343
|
+
if (nextSteps.length > 0) {
|
|
1344
|
+
emit.blank();
|
|
1345
|
+
emit.section("Next");
|
|
1346
|
+
for (const step of nextSteps) {
|
|
1347
|
+
emit.bullet(step);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return 0;
|
|
1351
|
+
}
|
|
1352
|
+
async function runSetup(options) {
|
|
1353
|
+
const emit = createEmitter(options);
|
|
1354
|
+
const runtime = new ManagedPlaywrightRuntime();
|
|
1355
|
+
const registrySources = await loadRegistrySources();
|
|
1356
|
+
const suggestedSource = registrySources.find((source) => source.authMode !== "legacy") ??
|
|
1357
|
+
registrySources[0];
|
|
1358
|
+
emit.title("Vana Connect setup");
|
|
1359
|
+
emit.blank();
|
|
1360
|
+
emit.section("Runtime");
|
|
1361
|
+
if (runtime.state === "installed") {
|
|
1362
|
+
emit.info("The local runtime is already installed.");
|
|
1363
|
+
if (runtime.runtimePath) {
|
|
1364
|
+
emit.keyValue("Browser", formatDisplayPath(runtime.runtimePath), "muted");
|
|
1365
|
+
}
|
|
1366
|
+
emit.blank();
|
|
1367
|
+
emit.section("Next");
|
|
1368
|
+
emit.bullet(`Check overall status with ${emit.code("vana status")}.`);
|
|
1369
|
+
emit.bullet(formatSetupConnectStep(emit, suggestedSource));
|
|
1370
|
+
emit.event({ type: "setup-check", runtime: runtime.state });
|
|
1371
|
+
return 0;
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
const result = await runtime.ensureInstalled(Boolean(options.yes));
|
|
1375
|
+
emit.success("Runtime ready.");
|
|
1376
|
+
if (result.logPath) {
|
|
1377
|
+
emit.detail(`Setup log: ${formatDisplayPath(result.logPath)}`);
|
|
1378
|
+
}
|
|
1379
|
+
emit.blank();
|
|
1380
|
+
emit.section("Next");
|
|
1381
|
+
emit.bullet(`Check overall status with ${emit.code("vana status")}.`);
|
|
1382
|
+
emit.bullet(formatSetupConnectStep(emit, suggestedSource));
|
|
1383
|
+
emit.event({
|
|
1384
|
+
type: "setup-complete",
|
|
1385
|
+
runtime: result.runtime,
|
|
1386
|
+
logPath: result.logPath,
|
|
1387
|
+
});
|
|
1388
|
+
return 0;
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
const message = error instanceof Error
|
|
1392
|
+
? error.message
|
|
1393
|
+
: "Vana Connect could not finish installing the local runtime.";
|
|
1394
|
+
emit.info(message);
|
|
1395
|
+
emit.event({
|
|
1396
|
+
type: "outcome",
|
|
1397
|
+
status: CliOutcomeStatus.RUNTIME_ERROR,
|
|
1398
|
+
reason: message,
|
|
1399
|
+
});
|
|
1400
|
+
return 1;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async function runDataList(options) {
|
|
1404
|
+
const state = await readCliState();
|
|
1405
|
+
const registrySources = await loadRegistrySources();
|
|
1406
|
+
const sources = await gatherSourceStatuses(state.sources, createSourceMetadataMap(registrySources));
|
|
1407
|
+
const datasetRecords = await Promise.all(sources
|
|
1408
|
+
.filter((source) => Boolean(source.lastResultPath))
|
|
1409
|
+
.map(async (source) => ({
|
|
1410
|
+
source: source.source,
|
|
1411
|
+
name: source.name,
|
|
1412
|
+
authMode: source.authMode ?? null,
|
|
1413
|
+
dataState: source.dataState,
|
|
1414
|
+
lastRunAt: source.lastRunAt ?? null,
|
|
1415
|
+
path: source.lastResultPath ?? null,
|
|
1416
|
+
summary: source.lastResultPath
|
|
1417
|
+
? await readResultSummary(source.lastResultPath)
|
|
1418
|
+
: null,
|
|
1419
|
+
})));
|
|
1420
|
+
datasetRecords.sort(compareDatasetOrder);
|
|
1421
|
+
const nextSteps = buildDataListNextSteps(datasetRecords, registrySources);
|
|
1422
|
+
if (options.json) {
|
|
1423
|
+
process.stdout.write(`${JSON.stringify({
|
|
1424
|
+
count: datasetRecords.length,
|
|
1425
|
+
latestDataset: datasetRecords[0] ?? null,
|
|
1426
|
+
nextSteps,
|
|
1427
|
+
summary: {
|
|
1428
|
+
localCount: datasetRecords.filter((dataset) => dataset.dataState !== "ingested_personal_server").length,
|
|
1429
|
+
syncedCount: datasetRecords.filter((dataset) => dataset.dataState === "ingested_personal_server").length,
|
|
1430
|
+
syncFailedCount: datasetRecords.filter((dataset) => dataset.dataState === "ingest_failed").length,
|
|
1431
|
+
},
|
|
1432
|
+
datasets: datasetRecords,
|
|
1433
|
+
})}\n`);
|
|
1434
|
+
return 0;
|
|
1435
|
+
}
|
|
1436
|
+
const emit = createEmitter(options);
|
|
1437
|
+
if (datasetRecords.length === 0) {
|
|
1438
|
+
const suggestedSource = registrySources.find((source) => source.authMode !== "legacy") ??
|
|
1439
|
+
registrySources[0];
|
|
1440
|
+
emit.title("Collected data");
|
|
1441
|
+
emit.blank();
|
|
1442
|
+
emit.info("No local datasets collected yet.");
|
|
1443
|
+
emit.blank();
|
|
1444
|
+
emit.section("Next");
|
|
1445
|
+
if (suggestedSource) {
|
|
1446
|
+
emit.bullet(`Collect your first dataset with ${emit.code(`vana connect ${suggestedSource.id}`)}.`);
|
|
1447
|
+
}
|
|
1448
|
+
else {
|
|
1449
|
+
emit.bullet(`Collect your first dataset with ${emit.code("vana connect")}.`);
|
|
1450
|
+
}
|
|
1451
|
+
emit.bullet(`Check overall status with ${emit.code("vana status")}.`);
|
|
1452
|
+
return 0;
|
|
1453
|
+
}
|
|
1454
|
+
emit.title(datasetRecords.length > 0
|
|
1455
|
+
? `Collected data (${datasetRecords.length})`
|
|
1456
|
+
: "Collected data");
|
|
1457
|
+
emit.blank();
|
|
1458
|
+
emit.info(joinOverviewParts([
|
|
1459
|
+
formatCountLabel("dataset", datasetRecords.length),
|
|
1460
|
+
formatCountLabel("local only", datasetRecords.filter((dataset) => dataset.dataState !== "ingested_personal_server").length),
|
|
1461
|
+
formatCountLabel("synced", datasetRecords.filter((dataset) => dataset.dataState === "ingested_personal_server").length),
|
|
1462
|
+
datasetRecords.some((dataset) => dataset.dataState === "ingest_failed")
|
|
1463
|
+
? formatCountLabel("sync failed", datasetRecords.filter((dataset) => dataset.dataState === "ingest_failed").length)
|
|
1464
|
+
: "",
|
|
1465
|
+
]));
|
|
1466
|
+
emit.blank();
|
|
1467
|
+
datasetRecords.forEach((dataset, index) => {
|
|
1468
|
+
if (index > 0) {
|
|
1469
|
+
emit.blank();
|
|
1470
|
+
}
|
|
1471
|
+
const badges = dataset.dataState === "ingested_personal_server"
|
|
1472
|
+
? [{ text: "synced", tone: "success" }]
|
|
1473
|
+
: dataset.dataState === "ingest_failed"
|
|
1474
|
+
? [{ text: "sync failed", tone: "warning" }]
|
|
1475
|
+
: [{ text: "local", tone: "muted" }];
|
|
1476
|
+
emit.sourceTitle(dataset.name ?? displaySource(dataset.source), badges);
|
|
1477
|
+
if (dataset.summary) {
|
|
1478
|
+
for (const line of dataset.summary.lines) {
|
|
1479
|
+
emit.detail(line);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
if (dataset.dataState === "ingested_personal_server") {
|
|
1483
|
+
emit.keyValue("State", "Synced to Personal Server", "success");
|
|
1484
|
+
}
|
|
1485
|
+
else if (dataset.dataState === "ingest_failed") {
|
|
1486
|
+
emit.keyValue("State", "Saved locally, sync failed", "warning");
|
|
1487
|
+
}
|
|
1488
|
+
else {
|
|
1489
|
+
emit.keyValue("State", "Saved locally", "muted");
|
|
1490
|
+
}
|
|
1491
|
+
if (dataset.lastRunAt) {
|
|
1492
|
+
emit.keyValue("Updated", formatTimestamp(dataset.lastRunAt), "muted");
|
|
1493
|
+
}
|
|
1494
|
+
if (dataset.path) {
|
|
1495
|
+
emit.keyValue("Path", formatDisplayPath(dataset.path), "muted");
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
emit.blank();
|
|
1499
|
+
emit.section("Next");
|
|
1500
|
+
for (const step of nextSteps) {
|
|
1501
|
+
emit.bullet(step);
|
|
1502
|
+
}
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
async function runDataShow(source, options) {
|
|
1506
|
+
const sourceLabels = createSourceLabelMap(await loadRegistrySources());
|
|
1507
|
+
const state = await readCliState();
|
|
1508
|
+
const record = state.sources[source];
|
|
1509
|
+
const resultPath = record?.lastResultPath;
|
|
1510
|
+
const datasetCount = Object.values(state.sources).filter((entry) => Boolean(entry?.lastResultPath)).length;
|
|
1511
|
+
const emit = createEmitter(options);
|
|
1512
|
+
if (!resultPath) {
|
|
1513
|
+
if (options.json) {
|
|
1514
|
+
process.stdout.write(`${JSON.stringify({
|
|
1515
|
+
error: "dataset_not_found",
|
|
1516
|
+
source,
|
|
1517
|
+
message: `No collected dataset found for ${displaySource(source, sourceLabels)}. Run \`vana connect ${source}\` first.`,
|
|
1518
|
+
nextSteps: [
|
|
1519
|
+
`Run \`vana connect ${source}\` to collect data.`,
|
|
1520
|
+
...(datasetCount > 0
|
|
1521
|
+
? ["Run `vana data list` to inspect other datasets."]
|
|
1522
|
+
: []),
|
|
1523
|
+
],
|
|
1524
|
+
})}\n`);
|
|
1525
|
+
}
|
|
1526
|
+
else {
|
|
1527
|
+
emit.info(`No collected dataset found for ${displaySource(source, sourceLabels)}. Run \`vana connect ${source}\` first.`);
|
|
1528
|
+
emit.blank();
|
|
1529
|
+
emit.section("Next");
|
|
1530
|
+
emit.bullet(`Collect data with ${emit.code(`vana connect ${source}`)}.`);
|
|
1531
|
+
if (datasetCount > 0) {
|
|
1532
|
+
emit.bullet(`Inspect other datasets with ${emit.code("vana data list")}.`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return 1;
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const raw = await fsp.readFile(resultPath, "utf8");
|
|
1539
|
+
const data = JSON.parse(raw);
|
|
1540
|
+
const summary = summarizeResultData(data);
|
|
1541
|
+
const nextSteps = buildDataShowNextSteps(source, datasetCount, sourceLabels);
|
|
1542
|
+
if (options.json) {
|
|
1543
|
+
process.stdout.write(`${JSON.stringify({
|
|
1544
|
+
source,
|
|
1545
|
+
name: displaySource(source, sourceLabels),
|
|
1546
|
+
path: resultPath,
|
|
1547
|
+
summary,
|
|
1548
|
+
lastRunAt: record?.lastRunAt ?? null,
|
|
1549
|
+
dataState: record?.dataState ?? null,
|
|
1550
|
+
nextSteps,
|
|
1551
|
+
data,
|
|
1552
|
+
})}\n`);
|
|
1553
|
+
return 0;
|
|
1554
|
+
}
|
|
1555
|
+
emit.title(`${displaySource(source, sourceLabels)} data`);
|
|
1556
|
+
emit.blank();
|
|
1557
|
+
if (summary) {
|
|
1558
|
+
emit.section("Summary");
|
|
1559
|
+
for (const line of summary.lines) {
|
|
1560
|
+
emit.bullet(line);
|
|
1561
|
+
}
|
|
1562
|
+
emit.blank();
|
|
1563
|
+
}
|
|
1564
|
+
emit.keyValue("Path", formatDisplayPath(resultPath), "muted");
|
|
1565
|
+
if (record?.lastRunAt) {
|
|
1566
|
+
emit.keyValue("Updated", formatTimestamp(record.lastRunAt), "muted");
|
|
1567
|
+
}
|
|
1568
|
+
if (record?.dataState === "ingested_personal_server") {
|
|
1569
|
+
emit.keyValue("State", "Synced to Personal Server", "success");
|
|
1570
|
+
}
|
|
1571
|
+
else if (record?.dataState === "ingest_failed") {
|
|
1572
|
+
emit.keyValue("State", "Saved locally, sync failed", "warning");
|
|
1573
|
+
}
|
|
1574
|
+
else {
|
|
1575
|
+
emit.keyValue("State", "Saved locally", "muted");
|
|
1576
|
+
}
|
|
1577
|
+
emit.blank();
|
|
1578
|
+
emit.section("Next");
|
|
1579
|
+
for (const step of nextSteps) {
|
|
1580
|
+
emit.bullet(step);
|
|
1581
|
+
}
|
|
1582
|
+
return 0;
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
const message = error instanceof Error ? error.message : `Could not read ${resultPath}.`;
|
|
1586
|
+
if (options.json) {
|
|
1587
|
+
process.stdout.write(`${JSON.stringify({ error: "dataset_read_failed", source, path: resultPath, message })}\n`);
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
emit.info(message);
|
|
1591
|
+
}
|
|
1592
|
+
return 1;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
async function runDataPath(source, options) {
|
|
1596
|
+
const sourceLabels = createSourceLabelMap(await loadRegistrySources());
|
|
1597
|
+
const state = await readCliState();
|
|
1598
|
+
const resultPath = state.sources[source]?.lastResultPath;
|
|
1599
|
+
if (!resultPath) {
|
|
1600
|
+
if (options.json) {
|
|
1601
|
+
process.stdout.write(`${JSON.stringify({
|
|
1602
|
+
error: "dataset_not_found",
|
|
1603
|
+
source,
|
|
1604
|
+
name: displaySource(source, sourceLabels),
|
|
1605
|
+
message: `No collected dataset found for ${displaySource(source, sourceLabels)}. Run \`vana connect ${source}\` first.`,
|
|
1606
|
+
})}\n`);
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
createEmitter(options).info(`No collected dataset found for ${displaySource(source, sourceLabels)}. Run \`vana connect ${source}\` first.`);
|
|
1610
|
+
}
|
|
1611
|
+
return 1;
|
|
1612
|
+
}
|
|
1613
|
+
if (options.json) {
|
|
1614
|
+
process.stdout.write(`${JSON.stringify({
|
|
1615
|
+
source,
|
|
1616
|
+
name: displaySource(source, sourceLabels),
|
|
1617
|
+
path: resultPath,
|
|
1618
|
+
lastRunAt: state.sources[source]?.lastRunAt ?? null,
|
|
1619
|
+
dataState: state.sources[source]?.dataState ?? null,
|
|
1620
|
+
nextSteps: [
|
|
1621
|
+
`Inspect the dataset with \`vana data show ${source}\`.`,
|
|
1622
|
+
`Reconnect ${displaySource(source, sourceLabels)} with \`vana connect ${source}\`.`,
|
|
1623
|
+
],
|
|
1624
|
+
})}\n`);
|
|
1625
|
+
}
|
|
1626
|
+
else {
|
|
1627
|
+
process.stdout.write(`${formatDisplayPath(resultPath)}\n`);
|
|
1628
|
+
}
|
|
1629
|
+
return 0;
|
|
1630
|
+
}
|
|
1631
|
+
async function runLogs(source, options) {
|
|
1632
|
+
const sourceLabels = createSourceLabelMap(await loadRegistrySources());
|
|
1633
|
+
const state = await readCliState();
|
|
1634
|
+
const records = Object.entries(state.sources)
|
|
1635
|
+
.filter(([, entry]) => Boolean(entry?.lastLogPath))
|
|
1636
|
+
.map(([sourceId, entry]) => ({
|
|
1637
|
+
source: sourceId,
|
|
1638
|
+
name: displaySource(sourceId, sourceLabels),
|
|
1639
|
+
path: entry?.lastLogPath ?? "",
|
|
1640
|
+
lastRunAt: entry?.lastRunAt ?? null,
|
|
1641
|
+
lastRunOutcome: entry?.lastRunOutcome ?? null,
|
|
1642
|
+
dataState: (entry?.dataState === "collected_local" ||
|
|
1643
|
+
entry?.dataState === "ingested_personal_server" ||
|
|
1644
|
+
entry?.dataState === "ingest_failed"
|
|
1645
|
+
? entry.dataState
|
|
1646
|
+
: null),
|
|
1647
|
+
}))
|
|
1648
|
+
.sort(compareLogRecordOrder);
|
|
1649
|
+
const logSummary = {
|
|
1650
|
+
attentionCount: records.filter((record) => isAttentionLog(record.lastRunOutcome, record.dataState)).length,
|
|
1651
|
+
successfulCount: records.filter((record) => record.dataState === "collected_local" ||
|
|
1652
|
+
record.dataState === "ingested_personal_server").length,
|
|
1653
|
+
localCount: records.filter((record) => record.dataState === "collected_local").length,
|
|
1654
|
+
syncedCount: records.filter((record) => record.dataState === "ingested_personal_server").length,
|
|
1655
|
+
};
|
|
1656
|
+
if (source) {
|
|
1657
|
+
const match = records.find((record) => record.source === source);
|
|
1658
|
+
if (!match) {
|
|
1659
|
+
const payload = {
|
|
1660
|
+
error: "log_not_found",
|
|
1661
|
+
source,
|
|
1662
|
+
message: `No stored run log found for ${displaySource(source, sourceLabels)}.`,
|
|
1663
|
+
nextSteps: [
|
|
1664
|
+
`Run \`vana connect ${source}\` to create a new log.`,
|
|
1665
|
+
...(records.length > 0
|
|
1666
|
+
? ["Run `vana logs` to inspect other logs."]
|
|
1667
|
+
: []),
|
|
1668
|
+
],
|
|
1669
|
+
};
|
|
1670
|
+
if (options.json) {
|
|
1671
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
const emit = createEmitter(options);
|
|
1675
|
+
emit.info(payload.message);
|
|
1676
|
+
emit.blank();
|
|
1677
|
+
emit.section("Next");
|
|
1678
|
+
for (const step of payload.nextSteps) {
|
|
1679
|
+
emit.bullet(step);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return 1;
|
|
1683
|
+
}
|
|
1684
|
+
if (options.json) {
|
|
1685
|
+
process.stdout.write(`${JSON.stringify(match)}\n`);
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
process.stdout.write(`${formatDisplayPath(match.path)}\n`);
|
|
1689
|
+
}
|
|
1690
|
+
return 0;
|
|
1691
|
+
}
|
|
1692
|
+
const nextSteps = buildLogsNextSteps(records);
|
|
1693
|
+
if (options.json) {
|
|
1694
|
+
process.stdout.write(`${JSON.stringify({
|
|
1695
|
+
count: records.length,
|
|
1696
|
+
latestLog: records[0] ?? null,
|
|
1697
|
+
nextSteps,
|
|
1698
|
+
summary: logSummary,
|
|
1699
|
+
logs: records,
|
|
1700
|
+
})}\n`);
|
|
1701
|
+
return 0;
|
|
1702
|
+
}
|
|
1703
|
+
const emit = createEmitter(options);
|
|
1704
|
+
emit.title(records.length > 0 ? `Run logs (${records.length})` : "Run logs");
|
|
1705
|
+
emit.blank();
|
|
1706
|
+
if (records.length === 0) {
|
|
1707
|
+
emit.info("No stored run logs yet.");
|
|
1708
|
+
emit.blank();
|
|
1709
|
+
emit.section("Next");
|
|
1710
|
+
for (const step of nextSteps) {
|
|
1711
|
+
emit.bullet(step);
|
|
1712
|
+
}
|
|
1713
|
+
return 0;
|
|
1714
|
+
}
|
|
1715
|
+
emit.info(joinOverviewParts([
|
|
1716
|
+
logSummary.attentionCount > 0
|
|
1717
|
+
? formatCountLabel("need attention", logSummary.attentionCount)
|
|
1718
|
+
: "",
|
|
1719
|
+
logSummary.successfulCount > 0
|
|
1720
|
+
? formatCountLabel("successful", logSummary.successfulCount)
|
|
1721
|
+
: "",
|
|
1722
|
+
logSummary.localCount > 0
|
|
1723
|
+
? formatCountLabel("local", logSummary.localCount)
|
|
1724
|
+
: "",
|
|
1725
|
+
logSummary.syncedCount > 0
|
|
1726
|
+
? formatCountLabel("synced", logSummary.syncedCount)
|
|
1727
|
+
: "",
|
|
1728
|
+
]));
|
|
1729
|
+
emit.blank();
|
|
1730
|
+
const groups = [
|
|
1731
|
+
{
|
|
1732
|
+
title: "Needs attention",
|
|
1733
|
+
items: records.filter((record) => isAttentionLog(record.lastRunOutcome, record.dataState)),
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
title: "Successful runs",
|
|
1737
|
+
items: records.filter((record) => !isAttentionLog(record.lastRunOutcome, record.dataState)),
|
|
1738
|
+
},
|
|
1739
|
+
].filter((group) => group.items.length > 0);
|
|
1740
|
+
groups.forEach((group, groupIndex) => {
|
|
1741
|
+
if (groupIndex > 0) {
|
|
1742
|
+
emit.blank();
|
|
1743
|
+
}
|
|
1744
|
+
emit.section(formatCountLabel(group.title, group.items.length));
|
|
1745
|
+
for (const record of group.items) {
|
|
1746
|
+
emit.sourceTitle(record.name, [
|
|
1747
|
+
{
|
|
1748
|
+
text: formatLogOutcomeLabel(record.lastRunOutcome, record.dataState),
|
|
1749
|
+
tone: toneForLogOutcome(record.lastRunOutcome, record.dataState),
|
|
1750
|
+
},
|
|
1751
|
+
]);
|
|
1752
|
+
emit.keyValue("Path", formatDisplayPath(record.path), "muted");
|
|
1753
|
+
if (record.lastRunAt) {
|
|
1754
|
+
emit.keyValue("Updated", formatTimestamp(record.lastRunAt), "muted");
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
});
|
|
1758
|
+
emit.blank();
|
|
1759
|
+
emit.section("Next");
|
|
1760
|
+
for (const step of nextSteps) {
|
|
1761
|
+
emit.bullet(step);
|
|
1762
|
+
}
|
|
1763
|
+
return 0;
|
|
1764
|
+
}
|
|
1765
|
+
function createEmitter(options) {
|
|
1766
|
+
const renderer = createHumanRenderer();
|
|
1767
|
+
return {
|
|
1768
|
+
event(event) {
|
|
1769
|
+
if (options.json) {
|
|
1770
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
info(message) {
|
|
1774
|
+
if (options.json || options.quiet) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
process.stdout.write(`${message}\n`);
|
|
1778
|
+
},
|
|
1779
|
+
blank() {
|
|
1780
|
+
if (options.json || options.quiet) {
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
process.stdout.write("\n");
|
|
1784
|
+
},
|
|
1785
|
+
title(message) {
|
|
1786
|
+
if (options.json || options.quiet) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
process.stdout.write(`${renderer.title(message)}\n`);
|
|
1790
|
+
},
|
|
1791
|
+
success(message) {
|
|
1792
|
+
if (options.json || options.quiet) {
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
process.stdout.write(`${renderer.success(message)}\n`);
|
|
1796
|
+
},
|
|
1797
|
+
section(message) {
|
|
1798
|
+
if (options.json || options.quiet) {
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
process.stdout.write(`${renderer.section(message)}\n`);
|
|
1802
|
+
},
|
|
1803
|
+
keyValue(label, value, tone = "muted") {
|
|
1804
|
+
if (options.json || options.quiet) {
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
process.stdout.write(`${renderer.keyValue(label, value, tone)}\n`);
|
|
1808
|
+
},
|
|
1809
|
+
detail(message) {
|
|
1810
|
+
if (options.json || options.quiet) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
process.stdout.write(`${renderer.detail(message)}\n`);
|
|
1814
|
+
},
|
|
1815
|
+
bullet(message) {
|
|
1816
|
+
if (options.json || options.quiet) {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
process.stdout.write(`${renderer.bullet(message)}\n`);
|
|
1820
|
+
},
|
|
1821
|
+
sourceTitle(name, badges = []) {
|
|
1822
|
+
if (options.json || options.quiet) {
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
process.stdout.write(`${renderer.sourceTitle(name, badges.map((badge) => renderer.badge(badge.text, badge.tone)))}\n`);
|
|
1826
|
+
},
|
|
1827
|
+
badge(text, tone = "muted") {
|
|
1828
|
+
return renderer.badge(text, tone);
|
|
1829
|
+
},
|
|
1830
|
+
code(text) {
|
|
1831
|
+
return renderer.theme.code(text);
|
|
1832
|
+
},
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function displaySource(source, labels = {}) {
|
|
1836
|
+
return labels[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
|
|
1837
|
+
}
|
|
1838
|
+
function formatCountLabel(label, count) {
|
|
1839
|
+
const normalizedLabel = label.charAt(0).toUpperCase() + label.slice(1);
|
|
1840
|
+
return `${normalizedLabel} (${count})`;
|
|
1841
|
+
}
|
|
1842
|
+
function joinOverviewParts(parts) {
|
|
1843
|
+
return parts.filter(Boolean).join(" • ");
|
|
1844
|
+
}
|
|
1845
|
+
function humanizeField(value) {
|
|
1846
|
+
return value
|
|
1847
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
1848
|
+
.replace(/[_-]/g, " ")
|
|
1849
|
+
.replace(/^\w/, (match) => match.toUpperCase());
|
|
1850
|
+
}
|
|
1851
|
+
function formatHumanSourceMessage(message, source, displayName) {
|
|
1852
|
+
if (!message || source === displayName) {
|
|
1853
|
+
return message;
|
|
1854
|
+
}
|
|
1855
|
+
return message.replace(new RegExp(`\\b${escapeRegExp(source)}\\b`, "gi"), displayName);
|
|
1856
|
+
}
|
|
1857
|
+
function escapeRegExp(value) {
|
|
1858
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1859
|
+
}
|
|
1860
|
+
function getErrorLogPath(error) {
|
|
1861
|
+
if (error &&
|
|
1862
|
+
typeof error === "object" &&
|
|
1863
|
+
"logPath" in error &&
|
|
1864
|
+
typeof error.logPath === "string") {
|
|
1865
|
+
return error.logPath;
|
|
1866
|
+
}
|
|
1867
|
+
return null;
|
|
1868
|
+
}
|
|
1869
|
+
async function gatherSourceStatuses(storedSources, metadata = {}) {
|
|
1870
|
+
const installedFiles = await listInstalledConnectorFiles();
|
|
1871
|
+
const sourceNames = new Set([
|
|
1872
|
+
...Object.keys(storedSources),
|
|
1873
|
+
...installedFiles.map((file) => file.source),
|
|
1874
|
+
]);
|
|
1875
|
+
return [...sourceNames]
|
|
1876
|
+
.map((source) => {
|
|
1877
|
+
const stored = storedSources[source] ?? {};
|
|
1878
|
+
const installed = installedFiles.some((file) => file.source === source);
|
|
1879
|
+
const details = metadata[source];
|
|
1880
|
+
const dataState = stored.dataState === "ingested_personal_server"
|
|
1881
|
+
? "ingested_personal_server"
|
|
1882
|
+
: stored.dataState === "ingest_failed"
|
|
1883
|
+
? "ingest_failed"
|
|
1884
|
+
: stored.dataState === "collected_local"
|
|
1885
|
+
? "collected_local"
|
|
1886
|
+
: "none";
|
|
1887
|
+
return {
|
|
1888
|
+
source,
|
|
1889
|
+
name: details?.name,
|
|
1890
|
+
company: details?.company,
|
|
1891
|
+
description: details?.description,
|
|
1892
|
+
authMode: details?.authMode ?? inferInstalledAuthMode(installedFiles, source),
|
|
1893
|
+
installed,
|
|
1894
|
+
sessionPresent: stored.sessionPresent ?? false,
|
|
1895
|
+
lastRunAt: stored.lastRunAt ?? null,
|
|
1896
|
+
lastRunOutcome: stored.lastRunOutcome ?? null,
|
|
1897
|
+
dataState,
|
|
1898
|
+
lastError: stored.lastError ?? null,
|
|
1899
|
+
lastResultPath: stored.lastResultPath ?? null,
|
|
1900
|
+
lastLogPath: stored.lastLogPath ?? null,
|
|
1901
|
+
};
|
|
1902
|
+
})
|
|
1903
|
+
.sort(compareSourceStatusOrder);
|
|
1904
|
+
}
|
|
1905
|
+
async function listInstalledConnectorFiles() {
|
|
1906
|
+
const connectorsDir = getConnectorCacheDir();
|
|
1907
|
+
try {
|
|
1908
|
+
const results = [];
|
|
1909
|
+
const entries = await fsp.readdir(connectorsDir, { withFileTypes: true });
|
|
1910
|
+
for (const entry of entries) {
|
|
1911
|
+
if (!entry.isDirectory()) {
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
const companyDir = path.join(connectorsDir, entry.name);
|
|
1915
|
+
const files = await fsp.readdir(companyDir);
|
|
1916
|
+
for (const file of files) {
|
|
1917
|
+
if (!file.endsWith("-playwright.js")) {
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
results.push({
|
|
1921
|
+
source: file.replace(/-playwright\.js$/, ""),
|
|
1922
|
+
path: path.join(companyDir, file),
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return results;
|
|
1927
|
+
}
|
|
1928
|
+
catch {
|
|
1929
|
+
return [];
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function formatSourceStatusDetails(source) {
|
|
1933
|
+
const details = [];
|
|
1934
|
+
const displayName = source.name ?? displaySource(source.source);
|
|
1935
|
+
if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
1936
|
+
details.push(source.lastError
|
|
1937
|
+
? {
|
|
1938
|
+
kind: "text",
|
|
1939
|
+
message: `${source.lastError}. Run \`vana connect ${source.source}\` interactively.`,
|
|
1940
|
+
}
|
|
1941
|
+
: {
|
|
1942
|
+
kind: "text",
|
|
1943
|
+
message: `Run \`vana connect ${source.source}\` interactively.`,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
1947
|
+
details.push({
|
|
1948
|
+
kind: "text",
|
|
1949
|
+
message: `Run \`vana connect ${source.source}\` without \`--no-input\` to complete the manual browser step.`,
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
1953
|
+
details.push(source.lastError
|
|
1954
|
+
? {
|
|
1955
|
+
kind: "text",
|
|
1956
|
+
message: formatHumanSourceMessage(source.lastError, source.source, displayName),
|
|
1957
|
+
}
|
|
1958
|
+
: {
|
|
1959
|
+
kind: "text",
|
|
1960
|
+
message: "The last connector run failed.",
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
1964
|
+
details.push(source.lastError
|
|
1965
|
+
? {
|
|
1966
|
+
kind: "text",
|
|
1967
|
+
message: formatHumanSourceMessage(source.lastError, source.source, displayName),
|
|
1968
|
+
}
|
|
1969
|
+
: {
|
|
1970
|
+
kind: "text",
|
|
1971
|
+
message: "No connector is available for this source.",
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
if (!source.lastRunOutcome && source.installed) {
|
|
1975
|
+
details.push({
|
|
1976
|
+
kind: "text",
|
|
1977
|
+
message: `Run \`vana connect ${source.source}\` to collect data.`,
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTED_LOCAL_ONLY &&
|
|
1981
|
+
source.lastResultPath) {
|
|
1982
|
+
details.push({
|
|
1983
|
+
kind: "text",
|
|
1984
|
+
message: `Inspect the latest local dataset with \`vana data show ${source.source}\`.`,
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
if (source.sessionPresent &&
|
|
1988
|
+
(source.lastRunOutcome === CliOutcomeStatus.CONNECTED_LOCAL_ONLY ||
|
|
1989
|
+
source.lastRunOutcome === CliOutcomeStatus.CONNECTED_AND_INGESTED ||
|
|
1990
|
+
source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED)) {
|
|
1991
|
+
details.push({
|
|
1992
|
+
kind: "row",
|
|
1993
|
+
label: "Session",
|
|
1994
|
+
value: "Saved for faster reconnects.",
|
|
1995
|
+
tone: "muted",
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTED_AND_INGESTED) {
|
|
1999
|
+
details.push({
|
|
2000
|
+
kind: "text",
|
|
2001
|
+
message: `Inspect the latest local dataset with \`vana data show ${source.source}\` or use your Personal Server copy.`,
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED) {
|
|
2005
|
+
details.push(source.lastError
|
|
2006
|
+
? {
|
|
2007
|
+
kind: "text",
|
|
2008
|
+
message: `${source.lastError} Inspect the local dataset with \`vana data show ${source.source}\`.`,
|
|
2009
|
+
}
|
|
2010
|
+
: {
|
|
2011
|
+
kind: "text",
|
|
2012
|
+
message: `Personal Server sync failed. Inspect the local dataset with \`vana data show ${source.source}\`.`,
|
|
2013
|
+
});
|
|
2014
|
+
}
|
|
2015
|
+
if (source.dataState === "ingested_personal_server") {
|
|
2016
|
+
details.push({
|
|
2017
|
+
kind: "row",
|
|
2018
|
+
label: "State",
|
|
2019
|
+
value: "Synced to Personal Server",
|
|
2020
|
+
tone: "success",
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
else if (source.dataState === "ingest_failed") {
|
|
2024
|
+
details.push({
|
|
2025
|
+
kind: "row",
|
|
2026
|
+
label: "State",
|
|
2027
|
+
value: "Saved locally, sync failed",
|
|
2028
|
+
tone: "warning",
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
else if (source.dataState === "collected_local") {
|
|
2032
|
+
details.push({
|
|
2033
|
+
kind: "row",
|
|
2034
|
+
label: "State",
|
|
2035
|
+
value: "Saved locally",
|
|
2036
|
+
tone: "muted",
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
if (source.lastRunAt) {
|
|
2040
|
+
details.push({
|
|
2041
|
+
kind: "row",
|
|
2042
|
+
label: "Updated",
|
|
2043
|
+
value: formatTimestamp(source.lastRunAt),
|
|
2044
|
+
tone: "muted",
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
if (source.lastResultPath && source.dataState !== "none") {
|
|
2048
|
+
details.push({
|
|
2049
|
+
kind: "row",
|
|
2050
|
+
label: "Path",
|
|
2051
|
+
value: formatDisplayPath(source.lastResultPath),
|
|
2052
|
+
tone: "muted",
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
if (source.lastLogPath &&
|
|
2056
|
+
source.lastRunOutcome &&
|
|
2057
|
+
source.lastRunOutcome !== CliOutcomeStatus.CONNECTED_LOCAL_ONLY &&
|
|
2058
|
+
source.lastRunOutcome !== CliOutcomeStatus.CONNECTED_AND_INGESTED) {
|
|
2059
|
+
details.push({
|
|
2060
|
+
kind: "row",
|
|
2061
|
+
label: "Run log",
|
|
2062
|
+
value: formatDisplayPath(source.lastLogPath),
|
|
2063
|
+
tone: "muted",
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
return details;
|
|
2067
|
+
}
|
|
2068
|
+
function buildStatusNextSteps(sources, sourceLabels = {}, runtime = "unhealthy", availableSources = []) {
|
|
2069
|
+
const nextSteps = [];
|
|
2070
|
+
const highestPriority = [...sources].sort(compareSourceStatusOrder)[0];
|
|
2071
|
+
const connectedSources = sources.filter((source) => source.dataState === "collected_local" ||
|
|
2072
|
+
source.dataState === "ingested_personal_server" ||
|
|
2073
|
+
source.dataState === "ingest_failed");
|
|
2074
|
+
const needsAttention = sources.some((source) => rankSourceStatus(source) <= 4);
|
|
2075
|
+
const highestPriorityLabel = highestPriority
|
|
2076
|
+
? displaySource(highestPriority.source, sourceLabels)
|
|
2077
|
+
: null;
|
|
2078
|
+
const suggestedSource = availableSources.find((source) => source.authMode !== "legacy") ??
|
|
2079
|
+
availableSources[0];
|
|
2080
|
+
if (!highestPriority) {
|
|
2081
|
+
if (runtime === "installed") {
|
|
2082
|
+
if (suggestedSource) {
|
|
2083
|
+
nextSteps.push(`Connect ${suggestedSource.name} with \`vana connect ${suggestedSource.id}\`.`);
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
nextSteps.push("Connect your first source with `vana connect`.");
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
else if (runtime === "missing") {
|
|
2090
|
+
nextSteps.push("Install the local runtime with `vana setup`.");
|
|
2091
|
+
nextSteps.push("Inspect install health with `vana doctor`.");
|
|
2092
|
+
}
|
|
2093
|
+
else if (runtime === "unhealthy") {
|
|
2094
|
+
nextSteps.push("Inspect install health with `vana doctor`.");
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
else if (highestPriority.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
2098
|
+
nextSteps.push(`Continue ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
|
|
2099
|
+
if (highestPriority.lastLogPath) {
|
|
2100
|
+
nextSteps.push(`Inspect the latest run log with \`vana logs ${highestPriority.source}\`.`);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
else if (highestPriority.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
2104
|
+
nextSteps.push(`Complete the manual browser step for ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
|
|
2105
|
+
if (highestPriority.lastLogPath) {
|
|
2106
|
+
nextSteps.push(`Inspect the latest run log with \`vana logs ${highestPriority.source}\`.`);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
else if (highestPriority.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
2110
|
+
nextSteps.push("Browse available sources with `vana sources`.");
|
|
2111
|
+
if (highestPriority.lastLogPath) {
|
|
2112
|
+
nextSteps.push(`Inspect the latest run log with \`vana logs ${highestPriority.source}\`.`);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
else if (highestPriority.dataState === "collected_local" ||
|
|
2116
|
+
highestPriority.dataState === "ingested_personal_server" ||
|
|
2117
|
+
highestPriority.dataState === "ingest_failed") {
|
|
2118
|
+
if (connectedSources.length > 1) {
|
|
2119
|
+
nextSteps.push("Review your collected data with `vana data list`.");
|
|
2120
|
+
}
|
|
2121
|
+
else {
|
|
2122
|
+
nextSteps.push(`Inspect the latest dataset with \`vana data show ${highestPriority.source}\`.`);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
if (connectedSources.length > 0 && needsAttention) {
|
|
2126
|
+
nextSteps.push(connectedSources.length > 1
|
|
2127
|
+
? "Review the data you already collected with `vana data list`."
|
|
2128
|
+
: `Inspect the data you already collected with \`vana data show ${connectedSources[0].source}\`.`);
|
|
2129
|
+
}
|
|
2130
|
+
if (sources.some((source) => source.installed || source.lastRunOutcome) &&
|
|
2131
|
+
(!needsAttention || connectedSources.length === 0)) {
|
|
2132
|
+
nextSteps.push("Connect another source with `vana sources`.");
|
|
2133
|
+
}
|
|
2134
|
+
if (runtime !== "installed" ||
|
|
2135
|
+
sources.some((source) => source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR ||
|
|
2136
|
+
source.lastRunOutcome === CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR)) {
|
|
2137
|
+
nextSteps.push("Inspect install health with `vana doctor`.");
|
|
2138
|
+
}
|
|
2139
|
+
return [...new Set(nextSteps)];
|
|
2140
|
+
}
|
|
2141
|
+
function buildSourcesNextSteps(recommendedSource, connectedCount) {
|
|
2142
|
+
const nextSteps = [];
|
|
2143
|
+
if (connectedCount > 0) {
|
|
2144
|
+
nextSteps.push("Inspect what you already collected with `vana data list`.");
|
|
2145
|
+
}
|
|
2146
|
+
if (recommendedSource) {
|
|
2147
|
+
nextSteps.push(`${recommendedSource.authMode === "legacy" ? "Complete" : "Connect"} ${recommendedSource.name} with \`vana connect ${recommendedSource.id}\`.`);
|
|
2148
|
+
}
|
|
2149
|
+
nextSteps.push("Or browse the guided picker with `vana connect`.");
|
|
2150
|
+
return [...new Set(nextSteps)];
|
|
2151
|
+
}
|
|
2152
|
+
function buildDataListNextSteps(datasetRecords, registrySources) {
|
|
2153
|
+
if (datasetRecords.length === 0) {
|
|
2154
|
+
const suggestedSource = registrySources.find((source) => source.authMode !== "legacy") ??
|
|
2155
|
+
registrySources[0];
|
|
2156
|
+
return [
|
|
2157
|
+
suggestedSource
|
|
2158
|
+
? `Collect your first dataset with \`vana connect ${suggestedSource.id}\`.`
|
|
2159
|
+
: "Collect your first dataset with `vana connect`.",
|
|
2160
|
+
"Check overall status with `vana status`.",
|
|
2161
|
+
];
|
|
2162
|
+
}
|
|
2163
|
+
return [
|
|
2164
|
+
`Inspect ${datasetRecords[0].name ?? displaySource(datasetRecords[0].source)} with \`vana data show ${datasetRecords[0].source}\`.`,
|
|
2165
|
+
`Or print its path with \`vana data path ${datasetRecords[0].source}\`.`,
|
|
2166
|
+
"Connect another source with `vana sources`.",
|
|
2167
|
+
];
|
|
2168
|
+
}
|
|
2169
|
+
function buildDataShowNextSteps(source, datasetCount, sourceLabels = {}) {
|
|
2170
|
+
return [
|
|
2171
|
+
`Print the path with \`vana data path ${source}\`.`,
|
|
2172
|
+
`Use \`vana data show ${source} --json | jq\` for structured inspection.`,
|
|
2173
|
+
`Reconnect ${displaySource(source, sourceLabels)} with \`vana connect ${source}\`.`,
|
|
2174
|
+
"Connect another source with `vana sources`.",
|
|
2175
|
+
...(datasetCount > 1
|
|
2176
|
+
? ["Inspect other datasets with `vana data list`."]
|
|
2177
|
+
: []),
|
|
2178
|
+
"Check overall status with `vana status`.",
|
|
2179
|
+
];
|
|
2180
|
+
}
|
|
2181
|
+
function buildLogsNextSteps(records) {
|
|
2182
|
+
if (records.length === 0) {
|
|
2183
|
+
return [
|
|
2184
|
+
"Run `vana connect <source>` to create a connector run log.",
|
|
2185
|
+
"Check overall status with `vana status`.",
|
|
2186
|
+
];
|
|
2187
|
+
}
|
|
2188
|
+
const attentionRecord = records.find((record) => isAttentionLog(record.lastRunOutcome, record.dataState));
|
|
2189
|
+
const successfulRecord = records.find((record) => !isAttentionLog(record.lastRunOutcome, record.dataState));
|
|
2190
|
+
return [
|
|
2191
|
+
attentionRecord
|
|
2192
|
+
? `Inspect the latest issue log with \`vana logs ${attentionRecord.source}\`.`
|
|
2193
|
+
: `Print the latest log path with \`vana logs ${records[0].source}\`.`,
|
|
2194
|
+
...(successfulRecord
|
|
2195
|
+
? [
|
|
2196
|
+
`Inspect a successful run with \`vana logs ${successfulRecord.source}\`.`,
|
|
2197
|
+
]
|
|
2198
|
+
: []),
|
|
2199
|
+
"Check overall status with `vana status`.",
|
|
2200
|
+
];
|
|
2201
|
+
}
|
|
2202
|
+
function formatSetupConnectStep(emit, source) {
|
|
2203
|
+
if (source) {
|
|
2204
|
+
return `Connect ${source.name} with ${emit.code(`vana connect ${source.id}`)}.`;
|
|
2205
|
+
}
|
|
2206
|
+
return `Connect a source with ${emit.code("vana connect")}.`;
|
|
2207
|
+
}
|
|
2208
|
+
function describeConnectTrust(authMode) {
|
|
2209
|
+
if (authMode === "legacy") {
|
|
2210
|
+
return "If needed, Vana Connect will open a local browser session on this machine.";
|
|
2211
|
+
}
|
|
2212
|
+
if (authMode === "interactive") {
|
|
2213
|
+
return "If needed, Vana Connect will ask for details in this terminal. Those details stay local to this machine.";
|
|
2214
|
+
}
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
function buildConnectChoices(sources, emit, recommendedSourceId = null) {
|
|
2218
|
+
const connected = sources.filter((source) => hasCollectedData(source.dataState));
|
|
2219
|
+
const readyNow = sources.filter((source) => source.authMode !== "legacy" && !hasCollectedData(source.dataState));
|
|
2220
|
+
const manualSteps = sources.filter((source) => source.authMode === "legacy" && !hasCollectedData(source.dataState));
|
|
2221
|
+
const choices = [];
|
|
2222
|
+
const appendGroup = (label, items) => {
|
|
2223
|
+
if (items.length === 0) {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (choices.length > 0) {
|
|
2227
|
+
choices.push(new Separator(""));
|
|
2228
|
+
}
|
|
2229
|
+
choices.push(new Separator(label));
|
|
2230
|
+
for (const item of items) {
|
|
2231
|
+
choices.push({
|
|
2232
|
+
name: `${item.name}${formatAuthModeBadge(item.authMode, emit)}${item.id === recommendedSourceId && item.authMode !== "legacy"
|
|
2233
|
+
? ` ${emit.badge("recommended", "accent")}`
|
|
2234
|
+
: ""}`,
|
|
2235
|
+
description: formatSourcePickerDescription(item),
|
|
2236
|
+
short: item.name,
|
|
2237
|
+
value: item.id,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
appendGroup("Connected", connected);
|
|
2242
|
+
appendGroup("Ready now", readyNow);
|
|
2243
|
+
appendGroup("Manual steps", manualSteps);
|
|
2244
|
+
return choices;
|
|
2245
|
+
}
|
|
2246
|
+
function formatMissingConnectSourceMessage(source) {
|
|
2247
|
+
if (source) {
|
|
2248
|
+
return `Specify a source. Start with \`vana connect ${source.id}\`, or run \`vana sources\` to see available options.`;
|
|
2249
|
+
}
|
|
2250
|
+
return "Specify a source. Run `vana sources` to see available options.";
|
|
2251
|
+
}
|
|
2252
|
+
function describeSourceFlow(authMode) {
|
|
2253
|
+
if (authMode === "legacy") {
|
|
2254
|
+
return "Flow: finishes with a manual browser step on this machine.";
|
|
2255
|
+
}
|
|
2256
|
+
if (authMode === "interactive") {
|
|
2257
|
+
return "Flow: prompts in this terminal when the source needs input.";
|
|
2258
|
+
}
|
|
2259
|
+
return "Flow: runs without extra input when the source supports it.";
|
|
2260
|
+
}
|
|
2261
|
+
function formatSourcePickerDescription(source) {
|
|
2262
|
+
if (hasCollectedData(source.dataState) && source.id) {
|
|
2263
|
+
const savedState = source.dataState === "ingested_personal_server"
|
|
2264
|
+
? "Already connected and synced."
|
|
2265
|
+
: source.dataState === "ingest_failed"
|
|
2266
|
+
? "Already connected locally; Personal Server sync failed."
|
|
2267
|
+
: "Already connected locally.";
|
|
2268
|
+
const reconnectHint = source.sessionPresent
|
|
2269
|
+
? " Saved session available for faster reconnects."
|
|
2270
|
+
: "";
|
|
2271
|
+
return `${savedState} Inspect with \`vana data show ${source.id}\` or reconnect now.${reconnectHint}`;
|
|
2272
|
+
}
|
|
2273
|
+
if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH && source.id) {
|
|
2274
|
+
return `Needs a manual browser step on this machine. Continue with \`vana connect ${source.id}\`.`;
|
|
2275
|
+
}
|
|
2276
|
+
const flow = describeSourceFlow(source.authMode);
|
|
2277
|
+
if (!source.description) {
|
|
2278
|
+
return flow;
|
|
2279
|
+
}
|
|
2280
|
+
return `${source.description} ${flow}`;
|
|
2281
|
+
}
|
|
2282
|
+
function normalizeArgv(argv) {
|
|
2283
|
+
if (argv[2] === "connect" &&
|
|
2284
|
+
["list", "status", "setup"].includes(argv[3] ?? "")) {
|
|
2285
|
+
const mapping = {
|
|
2286
|
+
list: "sources",
|
|
2287
|
+
status: "status",
|
|
2288
|
+
setup: "setup",
|
|
2289
|
+
};
|
|
2290
|
+
return [argv[0], argv[1], mapping[argv[3]], ...argv.slice(4)];
|
|
2291
|
+
}
|
|
2292
|
+
return argv;
|
|
2293
|
+
}
|
|
2294
|
+
function getCliVersion() {
|
|
2295
|
+
if (process.env.VANA_APP_ROOT) {
|
|
2296
|
+
try {
|
|
2297
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(process.env.VANA_APP_ROOT, "package.json"), "utf8"));
|
|
2298
|
+
if (packageJson.version) {
|
|
2299
|
+
return packageJson.version;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
catch {
|
|
2303
|
+
// Fall through to the repo/dev package metadata.
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
try {
|
|
2307
|
+
const packageJson = require("../../package.json");
|
|
2308
|
+
if (packageJson.version) {
|
|
2309
|
+
return packageJson.version;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
catch {
|
|
2313
|
+
// Fall through to the hard default.
|
|
2314
|
+
}
|
|
2315
|
+
return "0.0.0";
|
|
2316
|
+
}
|
|
2317
|
+
function getCliChannel(version = getCliVersion()) {
|
|
2318
|
+
if (version.includes("canary")) {
|
|
2319
|
+
return "canary";
|
|
2320
|
+
}
|
|
2321
|
+
const candidates = [process.env.VANA_APP_ROOT ?? "", process.execPath].map((value) => value.replace(/\\/g, "/").toLowerCase());
|
|
2322
|
+
return candidates.some((normalizedPath) => /\/releases\/canary-[^/]+(?:\/app)?$/.test(normalizedPath))
|
|
2323
|
+
? "canary"
|
|
2324
|
+
: "stable";
|
|
2325
|
+
}
|
|
2326
|
+
function getCliInstallMethod(execPath = process.execPath) {
|
|
2327
|
+
const candidates = [process.env.VANA_APP_ROOT ?? "", execPath].map((value) => value.replace(/\\/g, "/").toLowerCase());
|
|
2328
|
+
for (const normalizedPath of candidates) {
|
|
2329
|
+
if (!normalizedPath) {
|
|
2330
|
+
continue;
|
|
2331
|
+
}
|
|
2332
|
+
if (normalizedPath.includes("/cellar/vana/")) {
|
|
2333
|
+
return "homebrew";
|
|
2334
|
+
}
|
|
2335
|
+
if (normalizedPath.includes("/.local/share/vana/") ||
|
|
2336
|
+
normalizedPath.includes("/appdata/local/vana/") ||
|
|
2337
|
+
normalizedPath.endsWith("/current/app") ||
|
|
2338
|
+
/\/releases\/[^/]+\/app$/.test(normalizedPath)) {
|
|
2339
|
+
return "installer";
|
|
2340
|
+
}
|
|
2341
|
+
if (normalizedPath.endsWith("/node") ||
|
|
2342
|
+
normalizedPath.endsWith("/node.exe") ||
|
|
2343
|
+
normalizedPath.includes("/.nvm/") ||
|
|
2344
|
+
normalizedPath.includes("/volta/") ||
|
|
2345
|
+
normalizedPath.includes("/pnpm/")) {
|
|
2346
|
+
return "development";
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
return "unknown";
|
|
2350
|
+
}
|
|
2351
|
+
function getCliAppRoot(execPath = process.execPath) {
|
|
2352
|
+
return process.env.VANA_APP_ROOT ?? path.join(path.dirname(execPath), "app");
|
|
2353
|
+
}
|
|
2354
|
+
function getDoctorAppRootPath(installMethod, execPath = process.execPath) {
|
|
2355
|
+
if (process.env.VANA_APP_ROOT) {
|
|
2356
|
+
return process.env.VANA_APP_ROOT;
|
|
2357
|
+
}
|
|
2358
|
+
if (installMethod === "homebrew" || installMethod === "installer") {
|
|
2359
|
+
return getCliAppRoot(execPath);
|
|
2360
|
+
}
|
|
2361
|
+
return null;
|
|
2362
|
+
}
|
|
2363
|
+
function formatInstallMethodLabel(method) {
|
|
2364
|
+
switch (method) {
|
|
2365
|
+
case "homebrew":
|
|
2366
|
+
return "Homebrew";
|
|
2367
|
+
case "installer":
|
|
2368
|
+
return "Hosted installer";
|
|
2369
|
+
case "development":
|
|
2370
|
+
return "Development checkout";
|
|
2371
|
+
default:
|
|
2372
|
+
return "Unknown";
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
function getLifecycleCommands(installMethod, channel) {
|
|
2376
|
+
switch (installMethod) {
|
|
2377
|
+
case "homebrew":
|
|
2378
|
+
return {
|
|
2379
|
+
upgrade: "brew update && brew upgrade vana",
|
|
2380
|
+
uninstall: "brew uninstall vana",
|
|
2381
|
+
};
|
|
2382
|
+
case "installer":
|
|
2383
|
+
return {
|
|
2384
|
+
upgrade: channel === "canary"
|
|
2385
|
+
? "curl -fsSL https://raw.githubusercontent.com/vana-com/vana-connect/feat/connect-cli-v1/install/install.sh | sh -s -- --version canary-feat-connect-cli-v1"
|
|
2386
|
+
: "curl -fsSL https://raw.githubusercontent.com/vana-com/vana-connect/main/install/install.sh | sh",
|
|
2387
|
+
uninstall: "rm -f ~/.local/bin/vana && rm -rf ~/.local/share/vana ~/.dataconnect",
|
|
2388
|
+
};
|
|
2389
|
+
case "development":
|
|
2390
|
+
return {
|
|
2391
|
+
upgrade: "git pull && pnpm install && pnpm build",
|
|
2392
|
+
uninstall: "Remove the local checkout and any generated ~/.dataconnect state.",
|
|
2393
|
+
};
|
|
2394
|
+
default:
|
|
2395
|
+
return {
|
|
2396
|
+
upgrade: "Reinstall vana using Homebrew or the hosted installer.",
|
|
2397
|
+
uninstall: "Remove the installed vana binary and any ~/.dataconnect state you no longer need.",
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function extractGlobalOptions(argv) {
|
|
2402
|
+
return {
|
|
2403
|
+
json: argv.includes("--json"),
|
|
2404
|
+
noInput: argv.includes("--no-input"),
|
|
2405
|
+
yes: argv.includes("--yes"),
|
|
2406
|
+
quiet: argv.includes("--quiet"),
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
function createSourceLabelMap(sources) {
|
|
2410
|
+
return Object.fromEntries(sources.map((source) => [source.id, source.name]));
|
|
2411
|
+
}
|
|
2412
|
+
function createSourceMetadataMap(sources) {
|
|
2413
|
+
return Object.fromEntries(sources.map((source) => [
|
|
2414
|
+
source.id,
|
|
2415
|
+
{
|
|
2416
|
+
name: source.name,
|
|
2417
|
+
company: source.company,
|
|
2418
|
+
description: source.description,
|
|
2419
|
+
authMode: source.authMode,
|
|
2420
|
+
},
|
|
2421
|
+
]));
|
|
2422
|
+
}
|
|
2423
|
+
function formatAuthModeBadge(authMode, emit) {
|
|
2424
|
+
if (authMode === "legacy") {
|
|
2425
|
+
return ` ${emit ? emit.badge("legacy", "warning") : "[legacy]"}`;
|
|
2426
|
+
}
|
|
2427
|
+
if (authMode === "interactive") {
|
|
2428
|
+
return ` ${emit ? emit.badge("interactive", "info") : "[interactive]"}`;
|
|
2429
|
+
}
|
|
2430
|
+
return "";
|
|
2431
|
+
}
|
|
2432
|
+
function getSourceStatusPresentation(source) {
|
|
2433
|
+
if (!source.installed && !source.lastRunOutcome) {
|
|
2434
|
+
return { label: "not connected", tone: "muted" };
|
|
2435
|
+
}
|
|
2436
|
+
if (!source.lastRunOutcome) {
|
|
2437
|
+
return { label: "installed", tone: "success" };
|
|
2438
|
+
}
|
|
2439
|
+
if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
2440
|
+
return { label: "needs input", tone: "warning" };
|
|
2441
|
+
}
|
|
2442
|
+
if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
2443
|
+
return { label: "error", tone: "error" };
|
|
2444
|
+
}
|
|
2445
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
2446
|
+
return { label: "unavailable", tone: "warning" };
|
|
2447
|
+
}
|
|
2448
|
+
if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
2449
|
+
return { label: "manual step", tone: "warning" };
|
|
2450
|
+
}
|
|
2451
|
+
if (source.dataState === "ingested_personal_server") {
|
|
2452
|
+
return { label: "synced", tone: "success" };
|
|
2453
|
+
}
|
|
2454
|
+
if (source.dataState === "collected_local") {
|
|
2455
|
+
return { label: "local", tone: "muted" };
|
|
2456
|
+
}
|
|
2457
|
+
if (source.dataState === "ingest_failed") {
|
|
2458
|
+
return { label: "sync failed", tone: "warning" };
|
|
2459
|
+
}
|
|
2460
|
+
return { label: "connected", tone: "success" };
|
|
2461
|
+
}
|
|
2462
|
+
function toneForRuntime(runtime) {
|
|
2463
|
+
if (runtime === "installed") {
|
|
2464
|
+
return "success";
|
|
2465
|
+
}
|
|
2466
|
+
if (runtime === "missing") {
|
|
2467
|
+
return "warning";
|
|
2468
|
+
}
|
|
2469
|
+
return "muted";
|
|
2470
|
+
}
|
|
2471
|
+
function formatProgressUpdate(event) {
|
|
2472
|
+
const phaseLabel = event.phase &&
|
|
2473
|
+
typeof event.phase === "object" &&
|
|
2474
|
+
"label" in event.phase &&
|
|
2475
|
+
typeof event.phase.label === "string"
|
|
2476
|
+
? event.phase.label
|
|
2477
|
+
: null;
|
|
2478
|
+
const phaseStep = event.phase &&
|
|
2479
|
+
typeof event.phase === "object" &&
|
|
2480
|
+
"step" in event.phase &&
|
|
2481
|
+
typeof event.phase.step === "number"
|
|
2482
|
+
? event.phase.step
|
|
2483
|
+
: null;
|
|
2484
|
+
const phaseTotal = event.phase &&
|
|
2485
|
+
typeof event.phase === "object" &&
|
|
2486
|
+
"total" in event.phase &&
|
|
2487
|
+
typeof event.phase.total === "number"
|
|
2488
|
+
? event.phase.total
|
|
2489
|
+
: null;
|
|
2490
|
+
const phasePrefix = phaseLabel && phaseStep != null && phaseTotal != null
|
|
2491
|
+
? `${phaseLabel} (${phaseStep}/${phaseTotal})`
|
|
2492
|
+
: phaseLabel;
|
|
2493
|
+
if (phasePrefix && event.message) {
|
|
2494
|
+
return `${phasePrefix}: ${event.message}`;
|
|
2495
|
+
}
|
|
2496
|
+
if (event.message) {
|
|
2497
|
+
return event.message;
|
|
2498
|
+
}
|
|
2499
|
+
if (phasePrefix && typeof event.count === "number") {
|
|
2500
|
+
return `${phasePrefix}: ${event.count}`;
|
|
2501
|
+
}
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
function shouldRenderStatusUpdate(message) {
|
|
2505
|
+
return !/^complete\b/i.test(message.trim());
|
|
2506
|
+
}
|
|
2507
|
+
function inferInstalledAuthMode(installedFiles, source) {
|
|
2508
|
+
const match = installedFiles.find((file) => file.source === source);
|
|
2509
|
+
if (!match) {
|
|
2510
|
+
return undefined;
|
|
2511
|
+
}
|
|
2512
|
+
try {
|
|
2513
|
+
const script = fs.readFileSync(match.path, "utf8");
|
|
2514
|
+
if (/page\.requestInput\(/.test(script)) {
|
|
2515
|
+
return "interactive";
|
|
2516
|
+
}
|
|
2517
|
+
if (/page\.(showBrowser|promptUser)\(/.test(script)) {
|
|
2518
|
+
return "legacy";
|
|
2519
|
+
}
|
|
2520
|
+
return "automated";
|
|
2521
|
+
}
|
|
2522
|
+
catch {
|
|
2523
|
+
return undefined;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
async function loadRegistrySources() {
|
|
2527
|
+
try {
|
|
2528
|
+
return ((await listAvailableSources(findDataConnectorsDir() ?? undefined)) ?? []).sort(compareRegistrySourceOrder);
|
|
2529
|
+
}
|
|
2530
|
+
catch {
|
|
2531
|
+
return [];
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
function compareRegistrySourceOrder(left, right) {
|
|
2535
|
+
return (rankAuthMode(left.authMode) - rankAuthMode(right.authMode) ||
|
|
2536
|
+
left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
2537
|
+
}
|
|
2538
|
+
function compareSourceStatusOrder(left, right) {
|
|
2539
|
+
return (rankSourceStatus(left) - rankSourceStatus(right) ||
|
|
2540
|
+
compareRegistrySourceOrder({
|
|
2541
|
+
id: left.source,
|
|
2542
|
+
name: left.name ?? displaySource(left.source),
|
|
2543
|
+
authMode: left.authMode,
|
|
2544
|
+
}, {
|
|
2545
|
+
id: right.source,
|
|
2546
|
+
name: right.name ?? displaySource(right.source),
|
|
2547
|
+
authMode: right.authMode,
|
|
2548
|
+
}));
|
|
2549
|
+
}
|
|
2550
|
+
function rankSourceStatus(source) {
|
|
2551
|
+
if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
2552
|
+
return 0;
|
|
2553
|
+
}
|
|
2554
|
+
if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
2555
|
+
return 1;
|
|
2556
|
+
}
|
|
2557
|
+
if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED) {
|
|
2558
|
+
return 2;
|
|
2559
|
+
}
|
|
2560
|
+
if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
2561
|
+
return 3;
|
|
2562
|
+
}
|
|
2563
|
+
if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
2564
|
+
return 4;
|
|
2565
|
+
}
|
|
2566
|
+
if (source.dataState === "ingested_personal_server") {
|
|
2567
|
+
return 5;
|
|
2568
|
+
}
|
|
2569
|
+
if (source.dataState === "collected_local") {
|
|
2570
|
+
return 6;
|
|
2571
|
+
}
|
|
2572
|
+
if (source.installed) {
|
|
2573
|
+
return 7;
|
|
2574
|
+
}
|
|
2575
|
+
return 8;
|
|
2576
|
+
}
|
|
2577
|
+
function rankAuthMode(authMode) {
|
|
2578
|
+
if (authMode === "interactive") {
|
|
2579
|
+
return 0;
|
|
2580
|
+
}
|
|
2581
|
+
if (authMode === "automated") {
|
|
2582
|
+
return 1;
|
|
2583
|
+
}
|
|
2584
|
+
if (authMode === "legacy") {
|
|
2585
|
+
return 2;
|
|
2586
|
+
}
|
|
2587
|
+
return 3;
|
|
2588
|
+
}
|
|
2589
|
+
async function readResultSummary(resultPath) {
|
|
2590
|
+
try {
|
|
2591
|
+
const raw = await fsp.readFile(resultPath, "utf8");
|
|
2592
|
+
return summarizeResultData(JSON.parse(raw));
|
|
2593
|
+
}
|
|
2594
|
+
catch {
|
|
2595
|
+
return null;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function summarizeResultData(data) {
|
|
2599
|
+
const lines = [];
|
|
2600
|
+
const exportSummary = typeof data.exportSummary === "object" && data.exportSummary
|
|
2601
|
+
? data.exportSummary
|
|
2602
|
+
: null;
|
|
2603
|
+
const profile = typeof data.profile === "object" && data.profile
|
|
2604
|
+
? data.profile
|
|
2605
|
+
: null;
|
|
2606
|
+
if (profile?.username && typeof profile.username === "string") {
|
|
2607
|
+
lines.push(`Profile: ${profile.username}`);
|
|
2608
|
+
}
|
|
2609
|
+
if (Array.isArray(data.repositories)) {
|
|
2610
|
+
lines.push(`Repositories: ${data.repositories.length}`);
|
|
2611
|
+
const preview = summarizeNamedItems(data.repositories, "Latest repos");
|
|
2612
|
+
if (preview) {
|
|
2613
|
+
lines.push(preview);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (Array.isArray(data.starred)) {
|
|
2617
|
+
lines.push(`Starred: ${data.starred.length}`);
|
|
2618
|
+
}
|
|
2619
|
+
if (Array.isArray(data.orders)) {
|
|
2620
|
+
lines.push(`Orders: ${data.orders.length}`);
|
|
2621
|
+
}
|
|
2622
|
+
if (Array.isArray(data.playlists)) {
|
|
2623
|
+
lines.push(`Playlists: ${data.playlists.length}`);
|
|
2624
|
+
const preview = summarizeNamedItems(data.playlists, "Playlists");
|
|
2625
|
+
if (preview) {
|
|
2626
|
+
lines.push(preview);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (exportSummary?.details &&
|
|
2630
|
+
typeof exportSummary.details === "string" &&
|
|
2631
|
+
!lines.includes(exportSummary.details) &&
|
|
2632
|
+
!Array.isArray(data.repositories) &&
|
|
2633
|
+
!Array.isArray(data.starred) &&
|
|
2634
|
+
!Array.isArray(data.orders) &&
|
|
2635
|
+
!Array.isArray(data.playlists)) {
|
|
2636
|
+
lines.push(exportSummary.details);
|
|
2637
|
+
}
|
|
2638
|
+
return lines.length > 0 ? { lines } : null;
|
|
2639
|
+
}
|
|
2640
|
+
function summarizeNamedItems(items, label, maxItems = 2) {
|
|
2641
|
+
const names = items
|
|
2642
|
+
.map((item) => {
|
|
2643
|
+
if (typeof item === "object" &&
|
|
2644
|
+
item &&
|
|
2645
|
+
"name" in item &&
|
|
2646
|
+
typeof item.name === "string") {
|
|
2647
|
+
return item.name;
|
|
2648
|
+
}
|
|
2649
|
+
return null;
|
|
2650
|
+
})
|
|
2651
|
+
.filter((value) => Boolean(value))
|
|
2652
|
+
.slice(0, maxItems);
|
|
2653
|
+
if (names.length === 0) {
|
|
2654
|
+
return null;
|
|
2655
|
+
}
|
|
2656
|
+
return `${label}: ${names.join(", ")}`;
|
|
2657
|
+
}
|
|
2658
|
+
function formatTimestamp(value) {
|
|
2659
|
+
const date = new Date(value);
|
|
2660
|
+
if (Number.isNaN(date.getTime())) {
|
|
2661
|
+
return value;
|
|
2662
|
+
}
|
|
2663
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
2664
|
+
dateStyle: "medium",
|
|
2665
|
+
timeStyle: "short",
|
|
2666
|
+
}).format(date);
|
|
2667
|
+
}
|
|
2668
|
+
function compareDatasetOrder(left, right) {
|
|
2669
|
+
const leftTime = left.lastRunAt ? Date.parse(left.lastRunAt) : 0;
|
|
2670
|
+
const rightTime = right.lastRunAt ? Date.parse(right.lastRunAt) : 0;
|
|
2671
|
+
return (rightTime - leftTime ||
|
|
2672
|
+
(left.name ?? left.source).localeCompare(right.name ?? right.source, undefined, {
|
|
2673
|
+
sensitivity: "base",
|
|
2674
|
+
}));
|
|
2675
|
+
}
|
|
2676
|
+
function compareLogRecordOrder(left, right) {
|
|
2677
|
+
const leftTimestamp = left.lastRunAt ? Date.parse(left.lastRunAt) : 0;
|
|
2678
|
+
const rightTimestamp = right.lastRunAt ? Date.parse(right.lastRunAt) : 0;
|
|
2679
|
+
return (rightTimestamp - leftTimestamp ||
|
|
2680
|
+
left.source.localeCompare(right.source, undefined, {
|
|
2681
|
+
sensitivity: "base",
|
|
2682
|
+
}));
|
|
2683
|
+
}
|
|
2684
|
+
function hasCollectedData(dataState) {
|
|
2685
|
+
return (dataState === "collected_local" ||
|
|
2686
|
+
dataState === "ingested_personal_server" ||
|
|
2687
|
+
dataState === "ingest_failed");
|
|
2688
|
+
}
|
|
2689
|
+
function formatLogOutcomeLabel(lastRunOutcome, dataState) {
|
|
2690
|
+
if (lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
|
|
2691
|
+
return "unavailable";
|
|
2692
|
+
}
|
|
2693
|
+
if (lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
|
|
2694
|
+
return "manual step";
|
|
2695
|
+
}
|
|
2696
|
+
if (lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
2697
|
+
return "error";
|
|
2698
|
+
}
|
|
2699
|
+
if (lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
|
|
2700
|
+
return "needs input";
|
|
2701
|
+
}
|
|
2702
|
+
if (dataState === "ingested_personal_server") {
|
|
2703
|
+
return "synced";
|
|
2704
|
+
}
|
|
2705
|
+
if (dataState === "ingest_failed") {
|
|
2706
|
+
return "sync failed";
|
|
2707
|
+
}
|
|
2708
|
+
if (dataState === "collected_local") {
|
|
2709
|
+
return "local";
|
|
2710
|
+
}
|
|
2711
|
+
return "recent";
|
|
2712
|
+
}
|
|
2713
|
+
function isAttentionLog(lastRunOutcome, dataState) {
|
|
2714
|
+
return !(dataState === "collected_local" ||
|
|
2715
|
+
dataState === "ingested_personal_server" ||
|
|
2716
|
+
lastRunOutcome === CliOutcomeStatus.CONNECTED_LOCAL_ONLY ||
|
|
2717
|
+
lastRunOutcome === CliOutcomeStatus.CONNECTED_AND_INGESTED);
|
|
2718
|
+
}
|
|
2719
|
+
function toneForLogOutcome(lastRunOutcome, dataState) {
|
|
2720
|
+
if (lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
|
|
2721
|
+
return "error";
|
|
2722
|
+
}
|
|
2723
|
+
if (lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE ||
|
|
2724
|
+
lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH ||
|
|
2725
|
+
lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT ||
|
|
2726
|
+
dataState === "ingest_failed") {
|
|
2727
|
+
return "warning";
|
|
2728
|
+
}
|
|
2729
|
+
if (dataState === "ingested_personal_server") {
|
|
2730
|
+
return "success";
|
|
2731
|
+
}
|
|
2732
|
+
if (dataState === "collected_local") {
|
|
2733
|
+
return "muted";
|
|
2734
|
+
}
|
|
2735
|
+
return "muted";
|
|
2736
|
+
}
|
|
2737
|
+
function isPromptCancelled(error) {
|
|
2738
|
+
return (error instanceof Error &&
|
|
2739
|
+
(error.name === "ExitPromptError" || error.message.includes("SIGINT")));
|
|
2740
|
+
}
|
|
2741
|
+
//# sourceMappingURL=index.js.map
|