@opendatalabs/connect 0.7.0 → 0.8.1-canary.0014dd6

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 (107) hide show
  1. package/README.md +204 -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 +1353 -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/format.d.ts +21 -0
  19. package/dist/cli/render/format.d.ts.map +1 -0
  20. package/dist/cli/render/format.js +61 -0
  21. package/dist/cli/render/format.js.map +1 -0
  22. package/dist/cli/render/index.d.ts +5 -0
  23. package/dist/cli/render/index.d.ts.map +1 -0
  24. package/dist/cli/render/index.js +5 -0
  25. package/dist/cli/render/index.js.map +1 -0
  26. package/dist/cli/render/symbols.d.ts +11 -0
  27. package/dist/cli/render/symbols.d.ts.map +1 -0
  28. package/dist/cli/render/symbols.js +21 -0
  29. package/dist/cli/render/symbols.js.map +1 -0
  30. package/dist/cli/render/theme.d.ts +15 -0
  31. package/dist/cli/render/theme.d.ts.map +1 -0
  32. package/dist/cli/render/theme.js +36 -0
  33. package/dist/cli/render/theme.js.map +1 -0
  34. package/dist/connectors/index.d.ts +2 -0
  35. package/dist/connectors/index.d.ts.map +1 -0
  36. package/dist/connectors/index.js +2 -0
  37. package/dist/connectors/index.js.map +1 -0
  38. package/dist/connectors/registry.d.ts +30 -0
  39. package/dist/connectors/registry.d.ts.map +1 -0
  40. package/dist/connectors/registry.js +166 -0
  41. package/dist/connectors/registry.js.map +1 -0
  42. package/dist/core/cli-types.d.ts +125 -0
  43. package/dist/core/cli-types.d.ts.map +1 -0
  44. package/dist/core/cli-types.js +66 -0
  45. package/dist/core/cli-types.js.map +1 -0
  46. package/dist/core/index.d.ts +3 -0
  47. package/dist/core/index.d.ts.map +1 -1
  48. package/dist/core/index.js +3 -0
  49. package/dist/core/index.js.map +1 -1
  50. package/dist/core/paths.d.ts +8 -0
  51. package/dist/core/paths.d.ts.map +1 -0
  52. package/dist/core/paths.js +25 -0
  53. package/dist/core/paths.js.map +1 -0
  54. package/dist/core/state-store.d.ts +17 -0
  55. package/dist/core/state-store.d.ts.map +1 -0
  56. package/dist/core/state-store.js +23 -0
  57. package/dist/core/state-store.js.map +1 -0
  58. package/dist/core/types.d.ts +4 -0
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/personal-server/index.d.ts +8 -0
  61. package/dist/personal-server/index.d.ts.map +1 -0
  62. package/dist/personal-server/index.js +76 -0
  63. package/dist/personal-server/index.js.map +1 -0
  64. package/dist/react/useVanaData.d.ts +1 -1
  65. package/dist/react/useVanaData.d.ts.map +1 -1
  66. package/dist/react/useVanaData.js +22 -9
  67. package/dist/react/useVanaData.js.map +1 -1
  68. package/dist/runtime/core/contracts.d.ts +83 -0
  69. package/dist/runtime/core/contracts.d.ts.map +1 -0
  70. package/dist/runtime/core/contracts.js +2 -0
  71. package/dist/runtime/core/contracts.js.map +1 -0
  72. package/dist/runtime/core/index.d.ts +2 -0
  73. package/dist/runtime/core/index.d.ts.map +1 -0
  74. package/dist/runtime/core/index.js +2 -0
  75. package/dist/runtime/core/index.js.map +1 -0
  76. package/dist/runtime/index.d.ts +4 -0
  77. package/dist/runtime/index.d.ts.map +1 -0
  78. package/dist/runtime/index.js +3 -0
  79. package/dist/runtime/index.js.map +1 -0
  80. package/dist/runtime/managed-playwright.d.ts +33 -0
  81. package/dist/runtime/managed-playwright.d.ts.map +1 -0
  82. package/dist/runtime/managed-playwright.js +113 -0
  83. package/dist/runtime/managed-playwright.js.map +1 -0
  84. package/dist/runtime/playwright/browser.d.ts +11 -0
  85. package/dist/runtime/playwright/browser.d.ts.map +1 -0
  86. package/dist/runtime/playwright/browser.js +208 -0
  87. package/dist/runtime/playwright/browser.js.map +1 -0
  88. package/dist/runtime/playwright/in-process-run.d.ts +6 -0
  89. package/dist/runtime/playwright/in-process-run.d.ts.map +1 -0
  90. package/dist/runtime/playwright/in-process-run.js +550 -0
  91. package/dist/runtime/playwright/in-process-run.js.map +1 -0
  92. package/dist/runtime/playwright/index.d.ts +3 -0
  93. package/dist/runtime/playwright/index.d.ts.map +1 -0
  94. package/dist/runtime/playwright/index.js +3 -0
  95. package/dist/runtime/playwright/index.js.map +1 -0
  96. package/dist/runtime/repo-paths.d.ts +2 -0
  97. package/dist/runtime/repo-paths.d.ts.map +1 -0
  98. package/dist/runtime/repo-paths.js +36 -0
  99. package/dist/runtime/repo-paths.js.map +1 -0
  100. package/dist/server/config.d.ts +2 -0
  101. package/dist/server/config.d.ts.map +1 -1
  102. package/dist/server/config.js +1 -0
  103. package/dist/server/config.js.map +1 -1
  104. package/dist/server/connect.d.ts.map +1 -1
  105. package/dist/server/connect.js +7 -0
  106. package/dist/server/connect.js.map +1 -1
  107. package/package.json +43 -4
@@ -0,0 +1,1353 @@
1
+ import os from "node:os";
2
+ import fs from "node:fs";
3
+ import fsp from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { confirm, input, password, select } from "@inquirer/prompts";
6
+ import { Command } from "commander";
7
+ import { createHumanRenderer } from "./render/index.js";
8
+ import { CliOutcomeStatus, getBrowserProfilesDir, getConnectorCacheDir, getLastResultPath, readCliState, updateSourceState, } from "../core/index.js";
9
+ import { listAvailableSources } from "../connectors/registry.js";
10
+ import { detectPersonalServerTarget, ingestResult, } from "../personal-server/index.js";
11
+ import { findDataConnectorsDir, ManagedPlaywrightRuntime, } from "../runtime/index.js";
12
+ export async function runCli(argv = process.argv) {
13
+ const normalizedArgv = normalizeArgv(argv);
14
+ const parsedOptions = extractGlobalOptions(normalizedArgv);
15
+ const program = new Command();
16
+ program.name("vana").description("Vana Connect CLI");
17
+ program
18
+ .command("connect [source]")
19
+ .option("--json", "Output machine-readable JSON")
20
+ .option("--no-input", "Fail instead of prompting for input")
21
+ .option("--yes", "Approve safe setup prompts automatically")
22
+ .option("--quiet", "Reduce non-essential output")
23
+ .action(async (source) => {
24
+ process.exitCode = source
25
+ ? await runConnect(source, parsedOptions)
26
+ : await runConnectEntry(parsedOptions);
27
+ });
28
+ program
29
+ .command("sources")
30
+ .description("List supported sources")
31
+ .option("--json", "Output machine-readable JSON")
32
+ .action(async () => {
33
+ process.exitCode = await runList(parsedOptions);
34
+ });
35
+ program
36
+ .command("status")
37
+ .description("Show runtime and Personal Server status")
38
+ .option("--json", "Output machine-readable JSON")
39
+ .action(async () => {
40
+ process.exitCode = await runStatus(parsedOptions);
41
+ });
42
+ program
43
+ .command("setup")
44
+ .description("Install or repair the local runtime")
45
+ .option("--json", "Output machine-readable JSON")
46
+ .option("--yes", "Approve safe setup prompts automatically")
47
+ .action(async () => {
48
+ process.exitCode = await runSetup(parsedOptions);
49
+ });
50
+ const data = program.command("data").description("Inspect collected data");
51
+ data
52
+ .command("list")
53
+ .description("List locally available collected datasets")
54
+ .option("--json", "Output machine-readable JSON")
55
+ .action(async () => {
56
+ process.exitCode = await runDataList(parsedOptions);
57
+ });
58
+ data
59
+ .command("show <source>")
60
+ .description("Show a collected dataset")
61
+ .option("--json", "Output machine-readable JSON")
62
+ .action(async (source) => {
63
+ process.exitCode = await runDataShow(source, parsedOptions);
64
+ });
65
+ data
66
+ .command("path <source>")
67
+ .description("Print the local path for a collected dataset")
68
+ .option("--json", "Output machine-readable JSON")
69
+ .action(async (source) => {
70
+ process.exitCode = await runDataPath(source, parsedOptions);
71
+ });
72
+ await program.parseAsync(normalizedArgv);
73
+ return Number(process.exitCode ?? 0);
74
+ }
75
+ async function runConnect(source, options) {
76
+ const runtime = new ManagedPlaywrightRuntime();
77
+ const emit = createEmitter(options);
78
+ const registrySources = await loadRegistrySources();
79
+ const sourceLabels = createSourceLabelMap(registrySources);
80
+ const displayName = displaySource(source, sourceLabels);
81
+ let setupLogPath;
82
+ let fetchLogPath;
83
+ let runLogPath;
84
+ try {
85
+ emit.title(`Connect ${displayName}`);
86
+ emit.blank();
87
+ emit.section("Preparing");
88
+ emit.info(`Finding a connector for ${displayName}...`);
89
+ const target = await detectPersonalServerTarget();
90
+ if (runtime.state !== "installed") {
91
+ emit.info(`Vana Connect needs a local browser runtime before it can connect ${displayName}.`);
92
+ emit.blank();
93
+ emit.section("Runtime setup");
94
+ emit.bullet("Install the local browser runtime.");
95
+ emit.bullet("Install a Chromium browser engine.");
96
+ emit.bullet("Create local runtime files under `~/.dataconnect/`.");
97
+ emit.detail("Your credentials stay on this machine. Nothing is sent anywhere except the platform you’re connecting to.");
98
+ if (!options.yes) {
99
+ if (options.noInput) {
100
+ emit.event({
101
+ type: "outcome",
102
+ status: CliOutcomeStatus.SETUP_REQUIRED,
103
+ source,
104
+ });
105
+ return 1;
106
+ }
107
+ const shouldContinue = await confirm({
108
+ message: "Install the local runtime now?",
109
+ default: true,
110
+ });
111
+ if (!shouldContinue) {
112
+ emit.info("Cancelled. Runtime setup was not started.");
113
+ emit.event({
114
+ type: "outcome",
115
+ status: CliOutcomeStatus.SETUP_REQUIRED,
116
+ source,
117
+ reason: "setup_declined",
118
+ });
119
+ return 1;
120
+ }
121
+ }
122
+ const installResult = await runtime.ensureInstalled(Boolean(options.yes));
123
+ setupLogPath = installResult.logPath;
124
+ emit.event({
125
+ type: "setup-complete",
126
+ runtime: installResult.runtime,
127
+ logPath: installResult.logPath,
128
+ });
129
+ emit.success("Runtime ready.");
130
+ if (installResult.logPath) {
131
+ emit.detail(`Setup log: ${formatDisplayPath(installResult.logPath)}`);
132
+ }
133
+ }
134
+ else {
135
+ emit.event({
136
+ type: "setup-check",
137
+ runtime: runtime.state,
138
+ });
139
+ }
140
+ let fetched;
141
+ try {
142
+ fetched = await runtime.fetchConnector(source);
143
+ }
144
+ catch (error) {
145
+ const rawMessage = error instanceof Error
146
+ ? error.message
147
+ : `No connector is available for ${displayName} right now.`;
148
+ const message = formatHumanSourceMessage(rawMessage, source, displayName);
149
+ await updateSourceState(source, {
150
+ connectorInstalled: false,
151
+ lastRunAt: new Date().toISOString(),
152
+ lastRunOutcome: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
153
+ dataState: "none",
154
+ lastError: message,
155
+ lastResultPath: null,
156
+ });
157
+ emit.info(message);
158
+ if (!options.json) {
159
+ emit.blank();
160
+ emit.section("Next");
161
+ emit.bullet("Browse available sources with `vana sources`.");
162
+ emit.bullet(`Then connect one with ${emit.code("vana connect <source>")}.`);
163
+ }
164
+ emit.event({
165
+ type: "outcome",
166
+ status: CliOutcomeStatus.CONNECTOR_UNAVAILABLE,
167
+ source,
168
+ reason: message,
169
+ });
170
+ return 1;
171
+ }
172
+ fetchLogPath = fetched.logPath;
173
+ const sourceDetails = registrySources.find((item) => item.id === source);
174
+ const resolution = {
175
+ source,
176
+ connectorPath: fetched.connectorPath,
177
+ };
178
+ emit.event({
179
+ type: "connector-resolved",
180
+ source: resolution.source,
181
+ connectorPath: resolution.connectorPath,
182
+ logPath: fetched.logPath,
183
+ });
184
+ emit.info("Connector ready.");
185
+ if (sourceDetails?.description) {
186
+ emit.info(sourceDetails.description);
187
+ }
188
+ const profilePath = path.join(getBrowserProfilesDir(), `${path.basename(resolution.connectorPath, path.extname(resolution.connectorPath))}`);
189
+ if (fs.existsSync(profilePath)) {
190
+ emit.detail(`Found an existing ${displayName} session. Reusing it if it is still valid...`);
191
+ }
192
+ await updateSourceState(resolution.source, {
193
+ connectorInstalled: true,
194
+ sessionPresent: fs.existsSync(profilePath),
195
+ lastError: null,
196
+ });
197
+ emit.blank();
198
+ emit.section("Connecting");
199
+ emit.info(`Connecting to ${displayName}...`);
200
+ emit.info("Collecting your data...");
201
+ let finalStatus = CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR;
202
+ let finalDataState = "none";
203
+ let ingestFailureMessage = null;
204
+ let resultPath = getLastResultPath();
205
+ let collectedResult = false;
206
+ for await (const event of runtime.runConnector({
207
+ connectorPath: resolution.connectorPath,
208
+ source: resolution.source,
209
+ noInput: options.noInput,
210
+ onNeedInput: async (needInput) => {
211
+ emit.blank();
212
+ emit.section("Continue in this terminal");
213
+ emit.info(`Vana Connect will keep the ${displayName} session local to this machine.`);
214
+ emit.detail("The details you enter here stay local.");
215
+ emit.blank();
216
+ emit.info(needInput.message ??
217
+ `${displayName} needs additional details to continue.`);
218
+ const values = {};
219
+ for (const field of needInput.fields) {
220
+ if (field.toLowerCase().includes("password")) {
221
+ values[field] = await password({ message: humanizeField(field) });
222
+ }
223
+ else {
224
+ values[field] = await input({ message: humanizeField(field) });
225
+ }
226
+ }
227
+ return values;
228
+ },
229
+ })) {
230
+ emit.event(event);
231
+ if (event.logPath) {
232
+ runLogPath = event.logPath;
233
+ }
234
+ if (event.type === "needs-input") {
235
+ await updateSourceState(resolution.source, {
236
+ lastRunAt: new Date().toISOString(),
237
+ lastRunOutcome: CliOutcomeStatus.NEEDS_INPUT,
238
+ lastError: event.message ?? "Input required.",
239
+ });
240
+ emit.event({
241
+ type: "outcome",
242
+ status: CliOutcomeStatus.NEEDS_INPUT,
243
+ source: resolution.source,
244
+ });
245
+ if (!options.json) {
246
+ emit.info(`${displayName} needs additional input before it can connect.`);
247
+ emit.blank();
248
+ emit.section("Next");
249
+ emit.bullet(`Run ${emit.code(`vana connect ${source}`)} without ${emit.code("--no-input")}.`);
250
+ }
251
+ return 1;
252
+ }
253
+ if (event.type === "progress-update") {
254
+ const progressLine = formatProgressUpdate(event);
255
+ if (progressLine) {
256
+ emit.detail(progressLine);
257
+ }
258
+ continue;
259
+ }
260
+ if (event.type === "status-update") {
261
+ if (event.message && shouldRenderStatusUpdate(event.message)) {
262
+ emit.detail(event.message);
263
+ }
264
+ continue;
265
+ }
266
+ if (event.type === "runtime-error") {
267
+ await updateSourceState(resolution.source, {
268
+ lastRunAt: new Date().toISOString(),
269
+ lastRunOutcome: CliOutcomeStatus.RUNTIME_ERROR,
270
+ lastError: event.message ?? "Connector run failed.",
271
+ });
272
+ emit.info(event.message ?? "Connector run failed.");
273
+ emit.event({
274
+ type: "outcome",
275
+ status: CliOutcomeStatus.RUNTIME_ERROR,
276
+ source: resolution.source,
277
+ });
278
+ if (event.logPath) {
279
+ emit.info(`Run log: ${formatDisplayPath(event.logPath)}`);
280
+ }
281
+ return 1;
282
+ }
283
+ if (event.type === "headed-required") {
284
+ emit.blank();
285
+ emit.section("Continue in your browser");
286
+ if (event.message) {
287
+ emit.info(event.message);
288
+ }
289
+ if (event.url) {
290
+ emit.detail(`Opening ${displayName} in a local browser session...`);
291
+ }
292
+ continue;
293
+ }
294
+ if (event.type === "legacy-auth") {
295
+ await updateSourceState(resolution.source, {
296
+ lastRunAt: new Date().toISOString(),
297
+ lastRunOutcome: CliOutcomeStatus.LEGACY_AUTH,
298
+ lastError: event.message ?? "Legacy authentication is required.",
299
+ dataState: "none",
300
+ lastResultPath: null,
301
+ });
302
+ emit.info(event.message ??
303
+ "This connector needs a manual browser step that is not available in non-interactive mode.");
304
+ emit.blank();
305
+ emit.section("Next");
306
+ emit.bullet(`Run ${emit.code(`vana connect ${source}`)} without ${emit.code("--no-input")}.`);
307
+ if (event.logPath) {
308
+ emit.detail(`Run log: ${formatDisplayPath(event.logPath)}`);
309
+ }
310
+ emit.event({
311
+ type: "outcome",
312
+ status: CliOutcomeStatus.LEGACY_AUTH,
313
+ source: resolution.source,
314
+ });
315
+ return 1;
316
+ }
317
+ if (event.type === "collection-complete" && event.resultPath) {
318
+ collectedResult = true;
319
+ resultPath = event.resultPath;
320
+ const ingestEvents = await ingestResult(resolution.source, resultPath, target);
321
+ for (const ingestEvent of ingestEvents) {
322
+ emit.event(ingestEvent);
323
+ }
324
+ const ingestCompleted = ingestEvents.some((ingestEvent) => ingestEvent.type === "ingest-complete");
325
+ const ingestFailedEvent = ingestEvents.find((ingestEvent) => ingestEvent.type === "ingest-failed");
326
+ if (ingestCompleted) {
327
+ finalStatus = CliOutcomeStatus.CONNECTED_AND_INGESTED;
328
+ finalDataState = "ingested_personal_server";
329
+ }
330
+ else if (ingestFailedEvent?.type === "ingest-failed") {
331
+ finalStatus = CliOutcomeStatus.INGEST_FAILED;
332
+ finalDataState = "ingest_failed";
333
+ ingestFailureMessage =
334
+ ingestFailedEvent.message ?? "Personal Server sync failed.";
335
+ }
336
+ else {
337
+ finalStatus = CliOutcomeStatus.CONNECTED_LOCAL_ONLY;
338
+ finalDataState = "collected_local";
339
+ }
340
+ }
341
+ }
342
+ if (!collectedResult) {
343
+ await updateSourceState(resolution.source, {
344
+ connectorInstalled: true,
345
+ sessionPresent: fs.existsSync(profilePath),
346
+ lastRunAt: new Date().toISOString(),
347
+ lastRunOutcome: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
348
+ dataState: "none",
349
+ lastError: "Connector run ended without a result.",
350
+ lastResultPath: null,
351
+ });
352
+ emit.event({
353
+ type: "outcome",
354
+ status: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
355
+ source: resolution.source,
356
+ reason: "Connector run ended without a result.",
357
+ });
358
+ if (runLogPath) {
359
+ emit.info(`Run log: ${formatDisplayPath(runLogPath)}`);
360
+ }
361
+ return 1;
362
+ }
363
+ await updateSourceState(resolution.source, {
364
+ connectorInstalled: true,
365
+ sessionPresent: true,
366
+ lastRunAt: new Date().toISOString(),
367
+ lastRunOutcome: finalStatus,
368
+ dataState: finalDataState,
369
+ lastError: ingestFailureMessage,
370
+ lastResultPath: resultPath,
371
+ });
372
+ const resultSummary = await readResultSummary(resultPath);
373
+ const connectCommand = emit.code("vana status");
374
+ const dataCommand = emit.code(`vana data show ${source}`);
375
+ const successSummary = finalStatus === CliOutcomeStatus.CONNECTED_AND_INGESTED
376
+ ? `Collected your ${displayName} data and synced it to your Personal Server.`
377
+ : `Collected your ${displayName} data and saved it locally.`;
378
+ emit.success(`Connected ${displayName}.`);
379
+ emit.detail(successSummary);
380
+ emit.blank();
381
+ if (resultSummary) {
382
+ emit.section("Collected");
383
+ for (const line of resultSummary.lines) {
384
+ emit.bullet(line);
385
+ }
386
+ }
387
+ emit.blank();
388
+ if (finalStatus === CliOutcomeStatus.CONNECTED_AND_INGESTED) {
389
+ emit.section("Synced");
390
+ emit.bullet("Your data is now available in your Personal Server.");
391
+ }
392
+ else {
393
+ emit.section("Saved locally");
394
+ emit.bullet(formatDisplayPath(resultPath));
395
+ if (finalStatus === CliOutcomeStatus.INGEST_FAILED &&
396
+ ingestFailureMessage) {
397
+ emit.detail(`Personal Server sync failed: ${ingestFailureMessage}`);
398
+ }
399
+ else if (target.state !== "available") {
400
+ emit.detail("No Personal Server is available right now, so this run stayed local.");
401
+ }
402
+ }
403
+ if (runLogPath) {
404
+ emit.detail(`Run log: ${formatDisplayPath(runLogPath)}`);
405
+ }
406
+ else if (fetchLogPath) {
407
+ emit.detail(`Fetch log: ${formatDisplayPath(fetchLogPath)}`);
408
+ }
409
+ else if (setupLogPath) {
410
+ emit.detail(`Setup log: ${formatDisplayPath(setupLogPath)}`);
411
+ }
412
+ emit.blank();
413
+ emit.section("Next");
414
+ emit.bullet(`Run ${connectCommand}`);
415
+ emit.bullet(`Or inspect the data with ${dataCommand}`);
416
+ emit.event({
417
+ type: "outcome",
418
+ status: finalStatus,
419
+ source: resolution.source,
420
+ resultPath,
421
+ });
422
+ return 0;
423
+ }
424
+ catch (error) {
425
+ const message = error instanceof Error ? error.message : "Unexpected error.";
426
+ emit.info(message);
427
+ emit.event({
428
+ type: "outcome",
429
+ status: CliOutcomeStatus.UNEXPECTED_INTERNAL_ERROR,
430
+ source,
431
+ reason: message,
432
+ });
433
+ if (runLogPath) {
434
+ emit.detail(`Run log: ${formatDisplayPath(runLogPath)}`);
435
+ }
436
+ else if (fetchLogPath) {
437
+ emit.detail(`Fetch log: ${formatDisplayPath(fetchLogPath)}`);
438
+ }
439
+ else if (setupLogPath) {
440
+ emit.detail(`Setup log: ${formatDisplayPath(setupLogPath)}`);
441
+ }
442
+ return 1;
443
+ }
444
+ }
445
+ async function runConnectEntry(options) {
446
+ const emit = createEmitter(options);
447
+ const sources = await loadRegistrySources();
448
+ if (options.json) {
449
+ process.stdout.write(`${JSON.stringify({
450
+ error: "source_required",
451
+ message: "Specify a source. Run `vana sources` to see available options.",
452
+ })}\n`);
453
+ return 1;
454
+ }
455
+ if (options.noInput) {
456
+ emit.info("Specify a source. Run `vana sources` to see available options.");
457
+ return 1;
458
+ }
459
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
460
+ emit.info("Specify a source. Run `vana sources` to see available options.");
461
+ return 1;
462
+ }
463
+ if (sources.length === 0) {
464
+ emit.info("No sources are available right now.");
465
+ emit.info("Run `vana sources` to verify the local connector registry.");
466
+ return 1;
467
+ }
468
+ emit.title("Connect data");
469
+ emit.blank();
470
+ emit.info("Choose a source to connect:");
471
+ let source;
472
+ try {
473
+ source = await select({
474
+ message: "Source",
475
+ pageSize: 8,
476
+ choices: sources.map((item) => ({
477
+ name: `${item.name}${formatAuthModeBadge(item.authMode, emit)}`,
478
+ description: item.description,
479
+ short: item.name,
480
+ value: item.id,
481
+ })),
482
+ });
483
+ }
484
+ catch (error) {
485
+ if (isPromptCancelled(error)) {
486
+ emit.info("Cancelled. No source was connected.");
487
+ return 1;
488
+ }
489
+ throw error;
490
+ }
491
+ emit.blank();
492
+ return runConnect(source, options);
493
+ }
494
+ async function runList(options) {
495
+ const sources = await loadRegistrySources();
496
+ const installedSourceIds = new Set((await listInstalledConnectorFiles()).map((source) => source.source));
497
+ const enrichedSources = sources.map((source) => ({
498
+ ...source,
499
+ installed: installedSourceIds.has(source.id),
500
+ }));
501
+ if (options.json) {
502
+ process.stdout.write(`${JSON.stringify({ sources: enrichedSources })}\n`);
503
+ return 0;
504
+ }
505
+ const emit = createEmitter(options);
506
+ emit.title(enrichedSources.length > 0
507
+ ? `Available sources (${enrichedSources.length})`
508
+ : "Available sources");
509
+ emit.blank();
510
+ const groups = [
511
+ {
512
+ title: "Ready now",
513
+ items: enrichedSources.filter((source) => source.authMode !== "legacy"),
514
+ },
515
+ {
516
+ title: "Manual steps",
517
+ items: enrichedSources.filter((source) => source.authMode === "legacy"),
518
+ },
519
+ ].filter((group) => group.items.length > 0);
520
+ groups.forEach((group, index) => {
521
+ if (index > 0) {
522
+ emit.blank();
523
+ }
524
+ emit.section(formatCountLabel(group.title, group.items.length));
525
+ for (const source of group.items) {
526
+ const badges = [];
527
+ if (source.installed) {
528
+ badges.push({ text: "installed", tone: "success" });
529
+ }
530
+ if (source.authMode === "interactive") {
531
+ badges.push({ text: "interactive", tone: "info" });
532
+ }
533
+ else if (source.authMode === "legacy") {
534
+ badges.push({ text: "legacy", tone: "warning" });
535
+ }
536
+ emit.sourceTitle(source.name, badges);
537
+ if (source.description) {
538
+ emit.detail(source.description);
539
+ }
540
+ }
541
+ });
542
+ if (groups.length === 0) {
543
+ emit.info("No sources are available right now.");
544
+ }
545
+ else {
546
+ emit.blank();
547
+ emit.section("Next");
548
+ emit.bullet(`Connect one with ${emit.code("vana connect <source>")}.`);
549
+ }
550
+ return 0;
551
+ }
552
+ async function runStatus(options) {
553
+ const emit = createEmitter(options);
554
+ const runtime = new ManagedPlaywrightRuntime();
555
+ const personalServer = await detectPersonalServerTarget();
556
+ const state = await readCliState();
557
+ const registrySources = await loadRegistrySources();
558
+ const sourceLabels = createSourceLabelMap(registrySources);
559
+ const sourceMetadata = createSourceMetadataMap(registrySources);
560
+ const sources = await gatherSourceStatuses(state.sources, sourceMetadata);
561
+ const status = {
562
+ runtime: runtime.state,
563
+ runtimePath: runtime.runtimePath,
564
+ personalServer: personalServer.state,
565
+ personalServerUrl: personalServer.url,
566
+ sources,
567
+ };
568
+ if (options.json) {
569
+ process.stdout.write(`${JSON.stringify(status)}\n`);
570
+ return 0;
571
+ }
572
+ emit.title("Vana Connect status");
573
+ emit.blank();
574
+ emit.section("Environment");
575
+ emit.keyValue("Runtime", status.runtime, toneForRuntime(status.runtime));
576
+ if (status.runtimePath) {
577
+ emit.detail(formatDisplayPath(status.runtimePath));
578
+ }
579
+ emit.keyValue("Personal Server", status.personalServer, status.personalServer === "available" ? "success" : "muted");
580
+ if (status.personalServerUrl) {
581
+ emit.detail(status.personalServerUrl);
582
+ }
583
+ const sourceGroups = [
584
+ {
585
+ title: "Needs attention",
586
+ items: status.sources.filter((source) => rankSourceStatus(source) <= 4),
587
+ },
588
+ {
589
+ title: "Connected",
590
+ items: status.sources.filter((source) => source.dataState === "ingested_personal_server" ||
591
+ source.dataState === "collected_local" ||
592
+ source.dataState === "ingest_failed"),
593
+ },
594
+ {
595
+ title: "Installed",
596
+ items: status.sources.filter((source) => rankSourceStatus(source) > 4 &&
597
+ source.dataState === "none" &&
598
+ source.installed),
599
+ },
600
+ ].filter((group) => group.items.length > 0);
601
+ if (sourceGroups.length > 0) {
602
+ emit.blank();
603
+ emit.section(formatCountLabel("Sources", status.sources.length));
604
+ }
605
+ sourceGroups.forEach((group, index) => {
606
+ if (index > 0) {
607
+ emit.blank();
608
+ }
609
+ emit.section(formatCountLabel(group.title, group.items.length));
610
+ for (const source of group.items) {
611
+ const status = getSourceStatusPresentation(source);
612
+ const badges = [];
613
+ if (source.authMode === "interactive") {
614
+ badges.push({ text: "interactive", tone: "info" });
615
+ }
616
+ else if (source.authMode === "legacy") {
617
+ badges.push({ text: "legacy", tone: "warning" });
618
+ }
619
+ badges.push({ text: status.label, tone: status.tone });
620
+ emit.sourceTitle(displaySource(source.source, sourceLabels), badges);
621
+ const details = formatSourceStatusDetails(source);
622
+ for (const detail of details) {
623
+ emit.detail(detail);
624
+ }
625
+ }
626
+ });
627
+ const nextSteps = buildStatusNextSteps(status.sources, sourceLabels);
628
+ if (nextSteps.length > 0) {
629
+ emit.blank();
630
+ emit.section("Next");
631
+ for (const step of nextSteps) {
632
+ emit.bullet(step);
633
+ }
634
+ }
635
+ return 0;
636
+ }
637
+ async function runSetup(options) {
638
+ const emit = createEmitter(options);
639
+ const runtime = new ManagedPlaywrightRuntime();
640
+ if (runtime.state === "installed") {
641
+ emit.info("Vana Connect runtime is already installed.");
642
+ emit.event({ type: "setup-check", runtime: runtime.state });
643
+ return 0;
644
+ }
645
+ try {
646
+ const result = await runtime.ensureInstalled(Boolean(options.yes));
647
+ emit.success("Runtime ready.");
648
+ if (result.logPath) {
649
+ emit.info(`Setup log: ${formatDisplayPath(result.logPath)}`);
650
+ }
651
+ emit.event({
652
+ type: "setup-complete",
653
+ runtime: result.runtime,
654
+ logPath: result.logPath,
655
+ });
656
+ return 0;
657
+ }
658
+ catch (error) {
659
+ const message = error instanceof Error
660
+ ? error.message
661
+ : "Vana Connect could not finish installing the local runtime.";
662
+ emit.info(message);
663
+ emit.event({
664
+ type: "outcome",
665
+ status: CliOutcomeStatus.RUNTIME_ERROR,
666
+ reason: message,
667
+ });
668
+ return 1;
669
+ }
670
+ }
671
+ async function runDataList(options) {
672
+ const state = await readCliState();
673
+ const sources = await gatherSourceStatuses(state.sources, createSourceMetadataMap(await loadRegistrySources()));
674
+ const datasetRecords = await Promise.all(sources
675
+ .filter((source) => Boolean(source.lastResultPath))
676
+ .map(async (source) => ({
677
+ source: source.source,
678
+ name: source.name,
679
+ authMode: source.authMode ?? null,
680
+ dataState: source.dataState,
681
+ lastRunAt: source.lastRunAt ?? null,
682
+ path: source.lastResultPath ?? null,
683
+ summary: source.lastResultPath
684
+ ? await readResultSummary(source.lastResultPath)
685
+ : null,
686
+ })));
687
+ datasetRecords.sort(compareDatasetOrder);
688
+ if (options.json) {
689
+ process.stdout.write(`${JSON.stringify({ datasets: datasetRecords })}\n`);
690
+ return 0;
691
+ }
692
+ const emit = createEmitter(options);
693
+ if (datasetRecords.length === 0) {
694
+ emit.title("Collected data");
695
+ emit.blank();
696
+ emit.info("No local datasets collected yet.");
697
+ emit.info("Run `vana connect <source>` to collect data.");
698
+ return 0;
699
+ }
700
+ emit.title(datasetRecords.length > 0
701
+ ? `Collected data (${datasetRecords.length})`
702
+ : "Collected data");
703
+ emit.blank();
704
+ datasetRecords.forEach((dataset, index) => {
705
+ if (index > 0) {
706
+ emit.blank();
707
+ }
708
+ const badges = dataset.dataState === "ingested_personal_server"
709
+ ? [{ text: "synced", tone: "success" }]
710
+ : dataset.dataState === "ingest_failed"
711
+ ? [{ text: "sync failed", tone: "warning" }]
712
+ : [{ text: "local", tone: "muted" }];
713
+ emit.sourceTitle(dataset.name ?? displaySource(dataset.source), badges);
714
+ if (dataset.summary) {
715
+ for (const line of dataset.summary.lines) {
716
+ emit.detail(line);
717
+ }
718
+ }
719
+ if (dataset.lastRunAt) {
720
+ emit.keyValue("Updated", formatTimestamp(dataset.lastRunAt), "muted");
721
+ }
722
+ if (dataset.path) {
723
+ emit.keyValue("Path", formatDisplayPath(dataset.path), "muted");
724
+ }
725
+ });
726
+ emit.blank();
727
+ emit.section("Next");
728
+ emit.bullet(`Inspect one with ${emit.code("vana data show <source>")}.`);
729
+ emit.bullet(`Print a path with ${emit.code("vana data path <source>")}.`);
730
+ return 0;
731
+ }
732
+ async function runDataShow(source, options) {
733
+ const sourceLabels = createSourceLabelMap(await loadRegistrySources());
734
+ const state = await readCliState();
735
+ const record = state.sources[source];
736
+ const resultPath = record?.lastResultPath;
737
+ const emit = createEmitter(options);
738
+ if (!resultPath) {
739
+ if (options.json) {
740
+ process.stdout.write(`${JSON.stringify({
741
+ error: "dataset_not_found",
742
+ source,
743
+ message: `No collected dataset found for ${displaySource(source, sourceLabels)}. Run \`vana connect ${source}\` first.`,
744
+ })}\n`);
745
+ }
746
+ else {
747
+ emit.info(`No collected dataset found for ${displaySource(source, sourceLabels)}. Run \`vana connect ${source}\` first.`);
748
+ }
749
+ return 1;
750
+ }
751
+ try {
752
+ const raw = await fsp.readFile(resultPath, "utf8");
753
+ const data = JSON.parse(raw);
754
+ const summary = summarizeResultData(data);
755
+ if (options.json) {
756
+ process.stdout.write(`${JSON.stringify({
757
+ source,
758
+ name: displaySource(source, sourceLabels),
759
+ path: resultPath,
760
+ summary,
761
+ lastRunAt: record?.lastRunAt ?? null,
762
+ dataState: record?.dataState ?? null,
763
+ data,
764
+ })}\n`);
765
+ return 0;
766
+ }
767
+ emit.title(`${displaySource(source, sourceLabels)} data`);
768
+ emit.blank();
769
+ if (summary) {
770
+ emit.section("Summary");
771
+ for (const line of summary.lines) {
772
+ emit.bullet(line);
773
+ }
774
+ emit.blank();
775
+ }
776
+ emit.keyValue("Path", formatDisplayPath(resultPath), "muted");
777
+ if (record?.lastRunAt) {
778
+ emit.keyValue("Updated", formatTimestamp(record.lastRunAt), "muted");
779
+ }
780
+ if (record?.dataState === "ingested_personal_server") {
781
+ emit.keyValue("State", "Synced to Personal Server", "success");
782
+ }
783
+ else if (record?.dataState === "ingest_failed") {
784
+ emit.keyValue("State", "Saved locally, sync failed", "warning");
785
+ }
786
+ else {
787
+ emit.keyValue("State", "Saved locally", "muted");
788
+ }
789
+ emit.blank();
790
+ emit.section("Next");
791
+ emit.bullet(`Print the path with ${emit.code(`vana data path ${source}`)}.`);
792
+ emit.bullet(`Inspect other datasets with ${emit.code("vana data list")}.`);
793
+ emit.bullet(`Check overall status with ${emit.code("vana status")}.`);
794
+ return 0;
795
+ }
796
+ catch (error) {
797
+ const message = error instanceof Error ? error.message : `Could not read ${resultPath}.`;
798
+ if (options.json) {
799
+ process.stdout.write(`${JSON.stringify({ error: "dataset_read_failed", source, path: resultPath, message })}\n`);
800
+ }
801
+ else {
802
+ emit.info(message);
803
+ }
804
+ return 1;
805
+ }
806
+ }
807
+ async function runDataPath(source, options) {
808
+ const sourceLabels = createSourceLabelMap(await loadRegistrySources());
809
+ const state = await readCliState();
810
+ const resultPath = state.sources[source]?.lastResultPath;
811
+ if (!resultPath) {
812
+ if (options.json) {
813
+ process.stdout.write(`${JSON.stringify({
814
+ error: "dataset_not_found",
815
+ source,
816
+ name: displaySource(source, sourceLabels),
817
+ message: `No collected dataset found for ${displaySource(source, sourceLabels)}.`,
818
+ })}\n`);
819
+ }
820
+ else {
821
+ createEmitter(options).info(`No collected dataset found for ${displaySource(source, sourceLabels)}.`);
822
+ }
823
+ return 1;
824
+ }
825
+ if (options.json) {
826
+ process.stdout.write(`${JSON.stringify({
827
+ source,
828
+ name: displaySource(source, sourceLabels),
829
+ path: resultPath,
830
+ lastRunAt: state.sources[source]?.lastRunAt ?? null,
831
+ dataState: state.sources[source]?.dataState ?? null,
832
+ })}\n`);
833
+ }
834
+ else {
835
+ process.stdout.write(`${formatDisplayPath(resultPath)}\n`);
836
+ }
837
+ return 0;
838
+ }
839
+ function createEmitter(options) {
840
+ const renderer = createHumanRenderer();
841
+ return {
842
+ event(event) {
843
+ if (options.json) {
844
+ process.stdout.write(`${JSON.stringify(event)}\n`);
845
+ }
846
+ },
847
+ info(message) {
848
+ if (options.json || options.quiet) {
849
+ return;
850
+ }
851
+ process.stdout.write(`${message}\n`);
852
+ },
853
+ blank() {
854
+ if (options.json || options.quiet) {
855
+ return;
856
+ }
857
+ process.stdout.write("\n");
858
+ },
859
+ title(message) {
860
+ if (options.json || options.quiet) {
861
+ return;
862
+ }
863
+ process.stdout.write(`${renderer.title(message)}\n`);
864
+ },
865
+ success(message) {
866
+ if (options.json || options.quiet) {
867
+ return;
868
+ }
869
+ process.stdout.write(`${renderer.success(message)}\n`);
870
+ },
871
+ section(message) {
872
+ if (options.json || options.quiet) {
873
+ return;
874
+ }
875
+ process.stdout.write(`${renderer.section(message)}\n`);
876
+ },
877
+ keyValue(label, value, tone = "muted") {
878
+ if (options.json || options.quiet) {
879
+ return;
880
+ }
881
+ process.stdout.write(`${renderer.keyValue(label, value, tone)}\n`);
882
+ },
883
+ detail(message) {
884
+ if (options.json || options.quiet) {
885
+ return;
886
+ }
887
+ process.stdout.write(`${renderer.detail(message)}\n`);
888
+ },
889
+ bullet(message) {
890
+ if (options.json || options.quiet) {
891
+ return;
892
+ }
893
+ process.stdout.write(`${renderer.bullet(message)}\n`);
894
+ },
895
+ sourceTitle(name, badges = []) {
896
+ if (options.json || options.quiet) {
897
+ return;
898
+ }
899
+ process.stdout.write(`${renderer.sourceTitle(name, badges.map((badge) => renderer.badge(badge.text, badge.tone)))}\n`);
900
+ },
901
+ badge(text, tone = "muted") {
902
+ return renderer.badge(text, tone);
903
+ },
904
+ code(text) {
905
+ return renderer.theme.code(text);
906
+ },
907
+ };
908
+ }
909
+ function displaySource(source, labels = {}) {
910
+ return labels[source] ?? source.charAt(0).toUpperCase() + source.slice(1);
911
+ }
912
+ function formatCountLabel(label, count) {
913
+ return `${label} (${count})`;
914
+ }
915
+ function humanizeField(value) {
916
+ return value
917
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
918
+ .replace(/[_-]/g, " ")
919
+ .replace(/^\w/, (match) => match.toUpperCase());
920
+ }
921
+ function formatHumanSourceMessage(message, source, displayName) {
922
+ if (!message || source === displayName) {
923
+ return message;
924
+ }
925
+ return message.replace(new RegExp(`\\b${escapeRegExp(source)}\\b`, "gi"), displayName);
926
+ }
927
+ function escapeRegExp(value) {
928
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
929
+ }
930
+ async function gatherSourceStatuses(storedSources, metadata = {}) {
931
+ const installedFiles = await listInstalledConnectorFiles();
932
+ const sourceNames = new Set([
933
+ ...Object.keys(storedSources),
934
+ ...installedFiles.map((file) => file.source),
935
+ ]);
936
+ return [...sourceNames]
937
+ .map((source) => {
938
+ const stored = storedSources[source] ?? {};
939
+ const installed = installedFiles.some((file) => file.source === source);
940
+ const details = metadata[source];
941
+ const dataState = stored.dataState === "ingested_personal_server"
942
+ ? "ingested_personal_server"
943
+ : stored.dataState === "ingest_failed"
944
+ ? "ingest_failed"
945
+ : stored.dataState === "collected_local"
946
+ ? "collected_local"
947
+ : "none";
948
+ return {
949
+ source,
950
+ name: details?.name,
951
+ company: details?.company,
952
+ description: details?.description,
953
+ authMode: details?.authMode ?? inferInstalledAuthMode(installedFiles, source),
954
+ installed,
955
+ sessionPresent: stored.sessionPresent ?? false,
956
+ lastRunAt: stored.lastRunAt ?? null,
957
+ lastRunOutcome: stored.lastRunOutcome ?? null,
958
+ dataState,
959
+ lastError: stored.lastError ?? null,
960
+ lastResultPath: stored.lastResultPath ?? null,
961
+ };
962
+ })
963
+ .sort(compareSourceStatusOrder);
964
+ }
965
+ async function listInstalledConnectorFiles() {
966
+ const connectorsDir = getConnectorCacheDir();
967
+ try {
968
+ const results = [];
969
+ const entries = await fsp.readdir(connectorsDir, { withFileTypes: true });
970
+ for (const entry of entries) {
971
+ if (!entry.isDirectory()) {
972
+ continue;
973
+ }
974
+ const companyDir = path.join(connectorsDir, entry.name);
975
+ const files = await fsp.readdir(companyDir);
976
+ for (const file of files) {
977
+ if (!file.endsWith("-playwright.js")) {
978
+ continue;
979
+ }
980
+ results.push({
981
+ source: file.replace(/-playwright\.js$/, ""),
982
+ path: path.join(companyDir, file),
983
+ });
984
+ }
985
+ }
986
+ return results;
987
+ }
988
+ catch {
989
+ return [];
990
+ }
991
+ }
992
+ function formatSourceStatusDetails(source) {
993
+ const details = [];
994
+ const displayName = source.name ?? displaySource(source.source);
995
+ if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
996
+ details.push(source.lastError
997
+ ? `${source.lastError}. Run \`vana connect ${source.source}\` interactively.`
998
+ : `Run \`vana connect ${source.source}\` interactively.`);
999
+ }
1000
+ if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
1001
+ details.push(`Run \`vana connect ${source.source}\` without \`--no-input\` to complete the manual browser step.`);
1002
+ }
1003
+ if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
1004
+ details.push(source.lastError
1005
+ ? formatHumanSourceMessage(source.lastError, source.source, displayName)
1006
+ : "The last connector run failed.");
1007
+ }
1008
+ if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
1009
+ details.push(source.lastError
1010
+ ? formatHumanSourceMessage(source.lastError, source.source, displayName)
1011
+ : "No connector is available for this source.");
1012
+ }
1013
+ if (!source.lastRunOutcome && source.installed) {
1014
+ details.push(`Run \`vana connect ${source.source}\` to collect data.`);
1015
+ }
1016
+ if (source.lastRunOutcome === CliOutcomeStatus.CONNECTED_LOCAL_ONLY &&
1017
+ source.lastResultPath) {
1018
+ details.push(`Inspect the latest local dataset with \`vana data show ${source.source}\`.`);
1019
+ }
1020
+ if (source.sessionPresent &&
1021
+ (source.lastRunOutcome === CliOutcomeStatus.CONNECTED_LOCAL_ONLY ||
1022
+ source.lastRunOutcome === CliOutcomeStatus.CONNECTED_AND_INGESTED ||
1023
+ source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED)) {
1024
+ details.push("Saved browser session available for faster reconnects.");
1025
+ }
1026
+ if (source.lastRunOutcome === CliOutcomeStatus.CONNECTED_AND_INGESTED) {
1027
+ details.push(`Inspect the latest local dataset with \`vana data show ${source.source}\` or use your Personal Server copy.`);
1028
+ }
1029
+ if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED) {
1030
+ details.push(source.lastError
1031
+ ? `${source.lastError} Inspect the local dataset with \`vana data show ${source.source}\`.`
1032
+ : `Personal Server sync failed. Inspect the local dataset with \`vana data show ${source.source}\`.`);
1033
+ }
1034
+ if (source.lastRunAt) {
1035
+ details.push(`Updated: ${formatTimestamp(source.lastRunAt)}`);
1036
+ }
1037
+ if (source.lastResultPath && source.dataState !== "none") {
1038
+ details.push(formatDisplayPath(source.lastResultPath));
1039
+ }
1040
+ return details;
1041
+ }
1042
+ function buildStatusNextSteps(sources, sourceLabels = {}) {
1043
+ const nextSteps = [];
1044
+ const highestPriority = [...sources].sort(compareSourceStatusOrder)[0];
1045
+ const connectedSources = sources.filter((source) => source.dataState === "collected_local" ||
1046
+ source.dataState === "ingested_personal_server" ||
1047
+ source.dataState === "ingest_failed");
1048
+ const needsAttention = sources.some((source) => rankSourceStatus(source) <= 4);
1049
+ const highestPriorityLabel = highestPriority
1050
+ ? displaySource(highestPriority.source, sourceLabels)
1051
+ : null;
1052
+ if (highestPriority?.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
1053
+ nextSteps.push(`Continue ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
1054
+ }
1055
+ else if (highestPriority?.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
1056
+ nextSteps.push(`Complete the manual browser step for ${highestPriorityLabel} with \`vana connect ${highestPriority.source}\`.`);
1057
+ }
1058
+ else if (highestPriority?.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
1059
+ nextSteps.push("Browse available sources with `vana sources`.");
1060
+ }
1061
+ else if (highestPriority &&
1062
+ (highestPriority.dataState === "collected_local" ||
1063
+ highestPriority.dataState === "ingested_personal_server" ||
1064
+ highestPriority.dataState === "ingest_failed")) {
1065
+ if (connectedSources.length > 1) {
1066
+ nextSteps.push("Review your collected data with `vana data list`.");
1067
+ }
1068
+ else {
1069
+ nextSteps.push(`Inspect the latest dataset with \`vana data show ${highestPriority.source}\`.`);
1070
+ }
1071
+ }
1072
+ if (sources.some((source) => source.installed || source.lastRunOutcome) &&
1073
+ (!needsAttention || connectedSources.length === 0)) {
1074
+ nextSteps.push("Connect another source with `vana sources`.");
1075
+ }
1076
+ return [...new Set(nextSteps)];
1077
+ }
1078
+ function normalizeArgv(argv) {
1079
+ if (argv[2] === "connect" &&
1080
+ ["list", "status", "setup"].includes(argv[3] ?? "")) {
1081
+ const mapping = {
1082
+ list: "sources",
1083
+ status: "status",
1084
+ setup: "setup",
1085
+ };
1086
+ return [argv[0], argv[1], mapping[argv[3]], ...argv.slice(4)];
1087
+ }
1088
+ return argv;
1089
+ }
1090
+ function formatDisplayPath(filePath) {
1091
+ const homeDir = os.homedir();
1092
+ if (filePath === homeDir) {
1093
+ return "~";
1094
+ }
1095
+ if (filePath.startsWith(`${homeDir}${path.sep}`)) {
1096
+ return `~${filePath.slice(homeDir.length)}`;
1097
+ }
1098
+ return filePath;
1099
+ }
1100
+ function extractGlobalOptions(argv) {
1101
+ return {
1102
+ json: argv.includes("--json"),
1103
+ noInput: argv.includes("--no-input"),
1104
+ yes: argv.includes("--yes"),
1105
+ quiet: argv.includes("--quiet"),
1106
+ };
1107
+ }
1108
+ function createSourceLabelMap(sources) {
1109
+ return Object.fromEntries(sources.map((source) => [source.id, source.name]));
1110
+ }
1111
+ function createSourceMetadataMap(sources) {
1112
+ return Object.fromEntries(sources.map((source) => [
1113
+ source.id,
1114
+ {
1115
+ name: source.name,
1116
+ company: source.company,
1117
+ description: source.description,
1118
+ authMode: source.authMode,
1119
+ },
1120
+ ]));
1121
+ }
1122
+ function formatAuthModeBadge(authMode, emit) {
1123
+ if (authMode === "legacy") {
1124
+ return ` ${emit ? emit.badge("legacy", "warning") : "[legacy]"}`;
1125
+ }
1126
+ if (authMode === "interactive") {
1127
+ return ` ${emit ? emit.badge("interactive", "info") : "[interactive]"}`;
1128
+ }
1129
+ return "";
1130
+ }
1131
+ function getSourceStatusPresentation(source) {
1132
+ if (!source.installed && !source.lastRunOutcome) {
1133
+ return { label: "not connected", tone: "muted" };
1134
+ }
1135
+ if (!source.lastRunOutcome) {
1136
+ return { label: "installed", tone: "success" };
1137
+ }
1138
+ if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
1139
+ return { label: "needs input", tone: "warning" };
1140
+ }
1141
+ if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
1142
+ return { label: "error", tone: "error" };
1143
+ }
1144
+ if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
1145
+ return { label: "unavailable", tone: "warning" };
1146
+ }
1147
+ if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
1148
+ return { label: "manual step", tone: "warning" };
1149
+ }
1150
+ if (source.dataState === "ingested_personal_server") {
1151
+ return { label: "synced", tone: "success" };
1152
+ }
1153
+ if (source.dataState === "collected_local") {
1154
+ return { label: "local", tone: "muted" };
1155
+ }
1156
+ if (source.dataState === "ingest_failed") {
1157
+ return { label: "sync failed", tone: "warning" };
1158
+ }
1159
+ return { label: "connected", tone: "success" };
1160
+ }
1161
+ function toneForRuntime(runtime) {
1162
+ if (runtime === "installed") {
1163
+ return "success";
1164
+ }
1165
+ if (runtime === "missing") {
1166
+ return "warning";
1167
+ }
1168
+ return "muted";
1169
+ }
1170
+ function formatProgressUpdate(event) {
1171
+ const phaseLabel = event.phase &&
1172
+ typeof event.phase === "object" &&
1173
+ "label" in event.phase &&
1174
+ typeof event.phase.label === "string"
1175
+ ? event.phase.label
1176
+ : null;
1177
+ const phaseStep = event.phase &&
1178
+ typeof event.phase === "object" &&
1179
+ "step" in event.phase &&
1180
+ typeof event.phase.step === "number"
1181
+ ? event.phase.step
1182
+ : null;
1183
+ const phaseTotal = event.phase &&
1184
+ typeof event.phase === "object" &&
1185
+ "total" in event.phase &&
1186
+ typeof event.phase.total === "number"
1187
+ ? event.phase.total
1188
+ : null;
1189
+ const phasePrefix = phaseLabel && phaseStep != null && phaseTotal != null
1190
+ ? `${phaseLabel} (${phaseStep}/${phaseTotal})`
1191
+ : phaseLabel;
1192
+ if (phasePrefix && event.message) {
1193
+ return `${phasePrefix}: ${event.message}`;
1194
+ }
1195
+ if (event.message) {
1196
+ return event.message;
1197
+ }
1198
+ if (phasePrefix && typeof event.count === "number") {
1199
+ return `${phasePrefix}: ${event.count}`;
1200
+ }
1201
+ return null;
1202
+ }
1203
+ function shouldRenderStatusUpdate(message) {
1204
+ return !/^complete\b/i.test(message.trim());
1205
+ }
1206
+ function inferInstalledAuthMode(installedFiles, source) {
1207
+ const match = installedFiles.find((file) => file.source === source);
1208
+ if (!match) {
1209
+ return undefined;
1210
+ }
1211
+ try {
1212
+ const script = fs.readFileSync(match.path, "utf8");
1213
+ if (/page\.requestInput\(/.test(script)) {
1214
+ return "interactive";
1215
+ }
1216
+ if (/page\.(showBrowser|promptUser)\(/.test(script)) {
1217
+ return "legacy";
1218
+ }
1219
+ return "automated";
1220
+ }
1221
+ catch {
1222
+ return undefined;
1223
+ }
1224
+ }
1225
+ async function loadRegistrySources() {
1226
+ try {
1227
+ return ((await listAvailableSources(findDataConnectorsDir() ?? undefined)) ?? []).sort(compareRegistrySourceOrder);
1228
+ }
1229
+ catch {
1230
+ return [];
1231
+ }
1232
+ }
1233
+ function compareRegistrySourceOrder(left, right) {
1234
+ return (rankAuthMode(left.authMode) - rankAuthMode(right.authMode) ||
1235
+ left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
1236
+ }
1237
+ function compareSourceStatusOrder(left, right) {
1238
+ return (rankSourceStatus(left) - rankSourceStatus(right) ||
1239
+ compareRegistrySourceOrder({
1240
+ id: left.source,
1241
+ name: left.name ?? displaySource(left.source),
1242
+ authMode: left.authMode,
1243
+ }, {
1244
+ id: right.source,
1245
+ name: right.name ?? displaySource(right.source),
1246
+ authMode: right.authMode,
1247
+ }));
1248
+ }
1249
+ function rankSourceStatus(source) {
1250
+ if (source.lastRunOutcome === CliOutcomeStatus.NEEDS_INPUT) {
1251
+ return 0;
1252
+ }
1253
+ if (source.lastRunOutcome === CliOutcomeStatus.LEGACY_AUTH) {
1254
+ return 1;
1255
+ }
1256
+ if (source.lastRunOutcome === CliOutcomeStatus.INGEST_FAILED) {
1257
+ return 2;
1258
+ }
1259
+ if (source.lastRunOutcome === CliOutcomeStatus.RUNTIME_ERROR) {
1260
+ return 3;
1261
+ }
1262
+ if (source.lastRunOutcome === CliOutcomeStatus.CONNECTOR_UNAVAILABLE) {
1263
+ return 4;
1264
+ }
1265
+ if (source.dataState === "ingested_personal_server") {
1266
+ return 5;
1267
+ }
1268
+ if (source.dataState === "collected_local") {
1269
+ return 6;
1270
+ }
1271
+ if (source.installed) {
1272
+ return 7;
1273
+ }
1274
+ return 8;
1275
+ }
1276
+ function rankAuthMode(authMode) {
1277
+ if (authMode === "interactive") {
1278
+ return 0;
1279
+ }
1280
+ if (authMode === "automated") {
1281
+ return 1;
1282
+ }
1283
+ if (authMode === "legacy") {
1284
+ return 2;
1285
+ }
1286
+ return 3;
1287
+ }
1288
+ async function readResultSummary(resultPath) {
1289
+ try {
1290
+ const raw = await fsp.readFile(resultPath, "utf8");
1291
+ return summarizeResultData(JSON.parse(raw));
1292
+ }
1293
+ catch {
1294
+ return null;
1295
+ }
1296
+ }
1297
+ function summarizeResultData(data) {
1298
+ const lines = [];
1299
+ const exportSummary = typeof data.exportSummary === "object" && data.exportSummary
1300
+ ? data.exportSummary
1301
+ : null;
1302
+ const profile = typeof data.profile === "object" && data.profile
1303
+ ? data.profile
1304
+ : null;
1305
+ if (profile?.username && typeof profile.username === "string") {
1306
+ lines.push(`Profile: ${profile.username}`);
1307
+ }
1308
+ if (Array.isArray(data.repositories)) {
1309
+ lines.push(`Repositories: ${data.repositories.length}`);
1310
+ }
1311
+ if (Array.isArray(data.starred)) {
1312
+ lines.push(`Starred: ${data.starred.length}`);
1313
+ }
1314
+ if (Array.isArray(data.orders)) {
1315
+ lines.push(`Orders: ${data.orders.length}`);
1316
+ }
1317
+ if (Array.isArray(data.playlists)) {
1318
+ lines.push(`Playlists: ${data.playlists.length}`);
1319
+ }
1320
+ if (exportSummary?.details &&
1321
+ typeof exportSummary.details === "string" &&
1322
+ !lines.includes(exportSummary.details) &&
1323
+ !Array.isArray(data.repositories) &&
1324
+ !Array.isArray(data.starred) &&
1325
+ !Array.isArray(data.orders) &&
1326
+ !Array.isArray(data.playlists)) {
1327
+ lines.push(exportSummary.details);
1328
+ }
1329
+ return lines.length > 0 ? { lines } : null;
1330
+ }
1331
+ function formatTimestamp(value) {
1332
+ const date = new Date(value);
1333
+ if (Number.isNaN(date.getTime())) {
1334
+ return value;
1335
+ }
1336
+ return new Intl.DateTimeFormat(undefined, {
1337
+ dateStyle: "medium",
1338
+ timeStyle: "short",
1339
+ }).format(date);
1340
+ }
1341
+ function compareDatasetOrder(left, right) {
1342
+ const leftTime = left.lastRunAt ? Date.parse(left.lastRunAt) : 0;
1343
+ const rightTime = right.lastRunAt ? Date.parse(right.lastRunAt) : 0;
1344
+ return (rightTime - leftTime ||
1345
+ (left.name ?? left.source).localeCompare(right.name ?? right.source, undefined, {
1346
+ sensitivity: "base",
1347
+ }));
1348
+ }
1349
+ function isPromptCancelled(error) {
1350
+ return (error instanceof Error &&
1351
+ (error.name === "ExitPromptError" || error.message.includes("SIGINT")));
1352
+ }
1353
+ //# sourceMappingURL=index.js.map