@opendatalabs/connect 0.8.0 → 0.8.1-canary.00d0b27

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