From 9c7b4f9b8459b2b0167654e968832c3de134f4e2 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 13 May 2026 18:51:10 +0200 Subject: [PATCH] feat(ingest): use foreground view for interactive public ingest --- packages/cli/src/context-build-view.test.ts | 37 ++++++++++++++++ packages/cli/src/context-build-view.ts | 25 +++++++++-- packages/cli/src/public-ingest.test.ts | 47 ++++++++++++++++++++- packages/cli/src/public-ingest.ts | 45 ++++++++++++++++++++ 4 files changed, 149 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/context-build-view.test.ts b/packages/cli/src/context-build-view.test.ts index a6db9b9e..21c614c6 100644 --- a/packages/cli/src/context-build-view.test.ts +++ b/packages/cli/src/context-build-view.test.ts @@ -428,6 +428,43 @@ describe('runContextBuild', () => { expect(callOrder).toEqual(['warehouse', 'dbt_main']); }); + it('runs only the requested connection when foreground build receives a target', async () => { + const io = makeIo(); + const project = projectWithConnections({ + warehouse: { driver: 'postgres' }, + docs: { driver: 'notion' }, + }); + const executeTarget = vi.fn(async (target) => + successResult(target.connectionId, target.driver, target.operation), + ); + + await expect( + runContextBuild( + project, + { + projectDir: '/tmp/project', + inputMode: 'disabled', + targetConnectionId: 'warehouse', + all: false, + depth: 'fast', + queryHistory: 'default', + }, + io.io, + { executeTarget, now: () => 1000 }, + ), + ).resolves.toMatchObject({ exitCode: 0 }); + + expect(executeTarget).toHaveBeenCalledTimes(1); + expect(executeTarget.mock.calls[0]?.[0]).toMatchObject({ + connectionId: 'warehouse', + operation: 'database-ingest', + databaseDepth: 'fast', + }); + expect(io.stdout()).toContain('Databases:'); + expect(io.stdout()).toContain('warehouse'); + expect(io.stdout()).not.toContain('docs'); + }); + it('returns exit code 1 when any target fails', async () => { const io = makeIo(); const project = projectWithConnections({ diff --git a/packages/cli/src/context-build-view.ts b/packages/cli/src/context-build-view.ts index 23225ce4..2d8c36bf 100644 --- a/packages/cli/src/context-build-view.ts +++ b/packages/cli/src/context-build-view.ts @@ -39,6 +39,11 @@ export interface ContextBuildViewState { export interface ContextBuildArgs { projectDir: string; inputMode: 'auto' | 'disabled'; + targetConnectionId?: string; + all?: boolean; + depth?: Extract['depth']; + queryHistory?: Extract['queryHistory']; + queryHistoryWindowDays?: number; scanMode?: 'structural' | 'enriched'; detectRelationships?: boolean; } @@ -565,7 +570,15 @@ export async function runContextBuild( io: KtxCliIo, deps: ContextBuildDeps = {}, ): Promise { - const plan = buildPublicIngestPlan(project, { projectDir: args.projectDir, all: true }); + const plan = buildPublicIngestPlan(project, { + projectDir: args.projectDir, + ...(args.targetConnectionId ? { targetConnectionId: args.targetConnectionId } : {}), + all: args.all ?? true, + ...(args.depth ? { depth: args.depth } : {}), + ...(args.queryHistory ? { queryHistory: args.queryHistory } : {}), + ...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}), + ...(args.scanMode ? { scanMode: args.scanMode } : {}), + }); const state = initViewState(plan.targets); const isTTY = io.stdout.isTTY === true; const nowFn = deps.now ?? (() => Date.now()); @@ -614,11 +627,15 @@ export async function runContextBuild( const runArgs: Extract = { command: 'run', projectDir: args.projectDir, - all: true, + ...(args.targetConnectionId ? { targetConnectionId: args.targetConnectionId } : {}), + all: args.all ?? true, json: false, inputMode: args.inputMode, - scanMode: args.scanMode, - detectRelationships: args.detectRelationships, + ...(args.depth ? { depth: args.depth } : {}), + ...(args.queryHistory ? { queryHistory: args.queryHistory } : {}), + ...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}), + ...(args.scanMode ? { scanMode: args.scanMode } : {}), + ...(args.detectRelationships !== undefined ? { detectRelationships: args.detectRelationships } : {}), }; let hasFailure = false; diff --git a/packages/cli/src/public-ingest.test.ts b/packages/cli/src/public-ingest.test.ts index 8d596cc7..149a0a29 100644 --- a/packages/cli/src/public-ingest.test.ts +++ b/packages/cli/src/public-ingest.test.ts @@ -2,11 +2,19 @@ import { buildDefaultKtxProjectConfig, type KtxProjectConfig } from '@ktx/contex import { describe, expect, it, vi } from 'vitest'; import { buildPublicIngestPlan, type KtxPublicIngestProject, runKtxPublicIngest } from './public-ingest.js'; -function makeIo(options: { isTTY?: boolean } = {}) { +function makeIo(options: { isTTY?: boolean; interactive?: boolean } = {}) { let stdout = ''; let stderr = ''; return { io: { + ...(options.interactive + ? { + stdin: { + isTTY: true, + setRawMode: vi.fn(), + }, + } + : {}), stdout: { isTTY: options.isTTY, write: (chunk: string) => { @@ -399,6 +407,43 @@ describe('runKtxPublicIngest', () => { expect(io.stdout()).not.toContain('live-database'); }); + it('delegates interactive TTY public ingest to the foreground context-build view', async () => { + const io = makeIo({ isTTY: true, interactive: true }); + const project = projectWithConnections({ warehouse: { driver: 'postgres' } }); + const runContextBuild = vi.fn(async () => ({ exitCode: 0 })); + const runScan = vi.fn(async () => 0); + + await expect( + runKtxPublicIngest( + { + command: 'run', + projectDir: '/tmp/project', + targetConnectionId: 'warehouse', + all: false, + json: false, + inputMode: 'auto', + depth: 'fast', + queryHistory: 'default', + }, + io.io, + { loadProject: vi.fn(async () => project), runContextBuild, runScan }, + ), + ).resolves.toBe(0); + + expect(runContextBuild).toHaveBeenCalledWith( + project, + expect.objectContaining({ + projectDir: '/tmp/project', + targetConnectionId: 'warehouse', + all: false, + depth: 'fast', + queryHistory: 'default', + }), + io.io, + ); + expect(runScan).not.toHaveBeenCalled(); + }); + it('runs all independent targets and reports partial failures', async () => { const io = makeIo(); const project = projectWithConnections({ diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts index 2c1b7aea..2ccc6cb2 100644 --- a/packages/cli/src/public-ingest.ts +++ b/packages/cli/src/public-ingest.ts @@ -86,10 +86,27 @@ export interface KtxPublicIngestDeps { loadProject?: (options: Parameters[0]) => Promise; runScan?: (args: KtxScanArgs, io: KtxCliIo, deps?: KtxScanDeps) => Promise; runIngest?: (args: KtxIngestArgs, io: KtxCliIo, deps?: KtxIngestDeps) => Promise; + runContextBuild?: ( + project: KtxPublicIngestProject, + args: KtxPublicContextBuildArgs, + io: KtxCliIo, + ) => Promise<{ exitCode: number }>; scanProgress?: KtxProgressPort; ingestProgress?: (update: KtxIngestProgressUpdate) => void; } +interface KtxPublicContextBuildArgs { + projectDir: string; + inputMode: 'auto' | 'disabled'; + targetConnectionId?: string; + all?: boolean; + depth?: KtxPublicIngestDepth; + queryHistory?: KtxPublicIngestQueryHistoryFlag; + queryHistoryWindowDays?: number; + scanMode?: Extract['mode']; + detectRelationships?: boolean; +} + const sourceAdapterByDriver = new Map([ ['metabase', 'metabase'], ['local_metabase', 'metabase'], @@ -455,6 +472,13 @@ function sourceIngestOutputMode(args: Extract, + io: KtxCliIo, +): boolean { + return args.inputMode === 'auto' && args.json !== true && io.stdout.isTTY === true && hasInteractiveInput(io); +} + interface CapturedPublicIngestIo extends KtxCliIo { capturedOutput(): string; } @@ -597,6 +621,27 @@ export async function runKtxPublicIngest( const loadProject = deps.loadProject ?? loadKtxProject; const project = await loadProject({ projectDir: args.projectDir }); + if (shouldUseForegroundContextBuildView(args, io)) { + const { runContextBuild } = await import('./context-build-view.js'); + const contextBuild = deps.runContextBuild ?? runContextBuild; + const result = await contextBuild( + project, + { + projectDir: args.projectDir, + ...(args.targetConnectionId ? { targetConnectionId: args.targetConnectionId } : {}), + all: args.all, + inputMode: args.inputMode, + ...(args.depth ? { depth: args.depth } : {}), + ...(args.queryHistory ? { queryHistory: args.queryHistory } : {}), + ...(args.queryHistoryWindowDays !== undefined ? { queryHistoryWindowDays: args.queryHistoryWindowDays } : {}), + ...(args.scanMode ? { scanMode: args.scanMode } : {}), + ...(args.detectRelationships !== undefined ? { detectRelationships: args.detectRelationships } : {}), + }, + io, + ); + return result.exitCode; + } + const plan = buildPublicIngestPlan(project, args); const results: KtxPublicIngestTargetResult[] = [];