mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
fix(cli): auto-install runtime during setup (#116)
* fix(cli): auto-install runtime during setup * test: align docs smoke with readme
This commit is contained in:
parent
42b688e934
commit
a72fca2b32
11 changed files with 131 additions and 23 deletions
|
|
@ -994,6 +994,37 @@ describe('runContextBuild', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('threads the original runtime IO into captured target execution', async () => {
|
||||
const io = makeIo({ isTTY: true });
|
||||
const project = projectWithConnections({
|
||||
warehouse: { driver: 'postgres', context: { queryHistory: { enabled: true } } },
|
||||
});
|
||||
const executeTarget = vi.fn(async (target) => successResult(target.connectionId, target.driver, target.operation));
|
||||
|
||||
await runContextBuild(
|
||||
project,
|
||||
{
|
||||
projectDir: '/tmp/project',
|
||||
inputMode: 'auto',
|
||||
cliVersion: '0.2.0',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
},
|
||||
io.io,
|
||||
{ executeTarget, now: () => 1000 },
|
||||
);
|
||||
|
||||
expect(executeTarget).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ connectionId: 'warehouse' }),
|
||||
expect.objectContaining({ runtimeInstallPolicy: 'auto' }),
|
||||
expect.objectContaining({
|
||||
stdout: expect.objectContaining({ isTTY: false }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
runtimeIo: io.io,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls onSourceProgress when sources start and finish', async () => {
|
||||
const io = makeIo();
|
||||
const project = projectWithConnections({
|
||||
|
|
|
|||
|
|
@ -1022,6 +1022,7 @@ export async function runContextBuild(
|
|||
const progressDeps: KtxPublicIngestDeps = {
|
||||
scanProgress: createContextBuildProgressPort(updateSchemaPhase),
|
||||
ingestProgress: updateIngestPhase,
|
||||
runtimeIo: io,
|
||||
onPhaseStart,
|
||||
onPhaseEnd,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from '@ktx/context/ingest';
|
||||
import { initKtxProject, ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type KtxIngestArgs, runKtxIngest } from './ingest.js';
|
||||
import { type KtxIngestArgs, type KtxIngestDeps, runKtxIngest } from './ingest.js';
|
||||
import type { KtxCliLocalIngestAdaptersOptions } from './local-adapters.js';
|
||||
import {
|
||||
CliLookerSlWritingAgentRunner,
|
||||
|
|
@ -1108,6 +1108,7 @@ describe('runKtxIngest', () => {
|
|||
completedLocalBundleRun(input, input.jobId ?? 'local-job-1'),
|
||||
);
|
||||
const io = makeIo();
|
||||
const runtimeIo = makeIo({ isTTY: true });
|
||||
|
||||
await expect(
|
||||
runKtxIngest(
|
||||
|
|
@ -1125,6 +1126,9 @@ describe('runKtxIngest', () => {
|
|||
createAdapters,
|
||||
runLocalIngest: runLocal,
|
||||
jobIdFactory: () => 'local-job-1',
|
||||
runtimeIo: runtimeIo.io,
|
||||
} as KtxIngestDeps & {
|
||||
runtimeIo: typeof runtimeIo.io;
|
||||
},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
|
@ -1133,7 +1137,7 @@ describe('runKtxIngest', () => {
|
|||
cliVersion: '0.2.0',
|
||||
projectDir,
|
||||
installPolicy: 'auto',
|
||||
io: io.io,
|
||||
io: runtimeIo.io,
|
||||
};
|
||||
expect(createAdapters).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ projectDir }),
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export interface KtxIngestDeps {
|
|||
| 'pullConfigOptions'
|
||||
>;
|
||||
progress?: (update: KtxIngestProgressUpdate) => void;
|
||||
runtimeIo?: KtxIngestIo;
|
||||
}
|
||||
|
||||
function reportStatus(report: IngestReportSnapshot): 'done' | 'error' {
|
||||
|
|
@ -615,7 +616,7 @@ export async function runKtxIngest(
|
|||
(deps.runLocalIngest || deps.runLocalMetabaseIngest ? () => [] : createKtxCliLocalIngestAdapters);
|
||||
const executeLocalIngest = deps.runLocalIngest ?? runLocalIngest;
|
||||
const localIngestOptions = deps.localIngestOptions ?? {};
|
||||
const managedDaemon = managedDaemonOptionsForIngestRun(args, io);
|
||||
const managedDaemon = managedDaemonOptionsForIngestRun(args, deps.runtimeIo ?? io);
|
||||
const operationalLogger = createCliOperationalLogger(io, args.outputMode);
|
||||
const adapterOptions = {
|
||||
...(localIngestOptions.pullConfigOptions ?? {}),
|
||||
|
|
|
|||
|
|
@ -421,11 +421,18 @@ describe('runKtxPublicIngest', () => {
|
|||
|
||||
it('runs query history after schema ingest with current-run window override', async () => {
|
||||
const io = makeIo();
|
||||
const runtimeIo = makeIo({ isTTY: true });
|
||||
const project = deepReadyProject({
|
||||
warehouse: { driver: 'postgres', context: { queryHistory: { enabled: true, windowDays: 90 } } },
|
||||
});
|
||||
const runScan = vi.fn(async () => 0);
|
||||
const runIngest = vi.fn<NonNullable<KtxPublicIngestDeps['runIngest']>>(async () => 0);
|
||||
const deps = {
|
||||
loadProject: vi.fn(async () => project),
|
||||
runScan,
|
||||
runIngest,
|
||||
runtimeIo: runtimeIo.io,
|
||||
} as KtxPublicIngestDeps & { runtimeIo: typeof runtimeIo.io };
|
||||
|
||||
await expect(
|
||||
runKtxPublicIngest(
|
||||
|
|
@ -442,13 +449,14 @@ describe('runKtxPublicIngest', () => {
|
|||
queryHistoryWindowDays: 30,
|
||||
},
|
||||
io.io,
|
||||
{ loadProject: vi.fn(async () => project), runScan, runIngest },
|
||||
deps,
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(runScan).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ connectionId: 'warehouse', mode: 'enriched' }),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ runtimeIo: runtimeIo.io }),
|
||||
);
|
||||
expect(runIngest).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
|
@ -461,6 +469,7 @@ describe('runKtxPublicIngest', () => {
|
|||
historicSqlPullConfigOverride: expect.objectContaining({ dialect: 'postgres', windowDays: 30 }),
|
||||
}),
|
||||
expect.anything(),
|
||||
expect.objectContaining({ runtimeIo: runtimeIo.io }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export interface KtxPublicIngestDeps {
|
|||
) => Promise<{ exitCode: number }>;
|
||||
scanProgress?: KtxProgressPort;
|
||||
ingestProgress?: (update: KtxIngestProgressUpdate) => void;
|
||||
runtimeIo?: KtxCliIo;
|
||||
onPhaseStart?: (phaseKey: KtxPublicIngestPhaseKey) => void;
|
||||
onPhaseEnd?: (phaseKey: KtxPublicIngestPhaseKey, status: 'done' | 'failed' | 'skipped', summary?: string) => void;
|
||||
}
|
||||
|
|
@ -719,10 +720,13 @@ export async function executePublicIngestTarget(
|
|||
const runScan = deps.runScan ?? runKtxScan;
|
||||
const capturedScanIo = deps.scanProgress ? null : createCapturedPublicIngestIo();
|
||||
const scanIo = capturedScanIo ?? io;
|
||||
const scanDeps = {
|
||||
...(deps.scanProgress ? { progress: deps.scanProgress } : {}),
|
||||
...(deps.runtimeIo ? { runtimeIo: deps.runtimeIo } : {}),
|
||||
};
|
||||
deps.onPhaseStart?.('database-schema');
|
||||
const scanExitCode = deps.scanProgress
|
||||
? await runScan(scanArgs, scanIo, { progress: deps.scanProgress })
|
||||
: await runScan(scanArgs, scanIo);
|
||||
const scanExitCode =
|
||||
Object.keys(scanDeps).length > 0 ? await runScan(scanArgs, scanIo, scanDeps) : await runScan(scanArgs, scanIo);
|
||||
if (scanExitCode !== 0) {
|
||||
deps.onPhaseEnd?.('database-schema', 'failed');
|
||||
if (target.queryHistory?.enabled === true) {
|
||||
|
|
@ -759,10 +763,15 @@ export async function executePublicIngestTarget(
|
|||
};
|
||||
const capturedIngestIo = deps.ingestProgress ? null : createCapturedPublicIngestIo();
|
||||
const ingestIo = capturedIngestIo ?? io;
|
||||
const ingestDeps = {
|
||||
...(deps.ingestProgress ? { progress: deps.ingestProgress } : {}),
|
||||
...(deps.runtimeIo ? { runtimeIo: deps.runtimeIo } : {}),
|
||||
};
|
||||
deps.onPhaseStart?.('query-history');
|
||||
const qhExitCode = deps.ingestProgress
|
||||
? await runIngest(ingestArgs, ingestIo, { progress: deps.ingestProgress })
|
||||
: await runIngest(ingestArgs, ingestIo);
|
||||
const qhExitCode =
|
||||
Object.keys(ingestDeps).length > 0
|
||||
? await runIngest(ingestArgs, ingestIo, ingestDeps)
|
||||
: await runIngest(ingestArgs, ingestIo);
|
||||
if (qhExitCode !== 0) {
|
||||
deps.onPhaseEnd?.('query-history', 'failed');
|
||||
return markTargetResult(
|
||||
|
|
@ -795,10 +804,15 @@ export async function executePublicIngestTarget(
|
|||
const runIngest = deps.runIngest ?? runKtxIngest;
|
||||
const capturedIngestIo = deps.ingestProgress ? null : createCapturedPublicIngestIo();
|
||||
const ingestIo = capturedIngestIo ?? io;
|
||||
const ingestDeps = {
|
||||
...(deps.ingestProgress ? { progress: deps.ingestProgress } : {}),
|
||||
...(deps.runtimeIo ? { runtimeIo: deps.runtimeIo } : {}),
|
||||
};
|
||||
deps.onPhaseStart?.('source-ingest');
|
||||
const exitCode = deps.ingestProgress
|
||||
? await runIngest(ingestArgs, ingestIo, { progress: deps.ingestProgress })
|
||||
: await runIngest(ingestArgs, ingestIo);
|
||||
const exitCode =
|
||||
Object.keys(ingestDeps).length > 0
|
||||
? await runIngest(ingestArgs, ingestIo, ingestDeps)
|
||||
: await runIngest(ingestArgs, ingestIo);
|
||||
deps.onPhaseEnd?.('source-ingest', exitCode === 0 ? 'done' : 'failed');
|
||||
return markTargetResult(
|
||||
target,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
RunLocalScanOptions,
|
||||
} from '@ktx/context/scan';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createCliScanProgress, runKtxScan } from './scan.js';
|
||||
import { createCliScanProgress, runKtxScan, type KtxScanDeps } from './scan.js';
|
||||
|
||||
const sqlServerExtractSchema = vi.hoisted(() =>
|
||||
vi.fn(async (connectionId: string) => ({
|
||||
|
|
@ -392,6 +392,7 @@ describe('runKtxScan', () => {
|
|||
}),
|
||||
);
|
||||
const io = makeIo();
|
||||
const runtimeIo = makeIo({ isTTY: true });
|
||||
|
||||
await expect(
|
||||
runKtxScan(
|
||||
|
|
@ -406,7 +407,9 @@ describe('runKtxScan', () => {
|
|||
runtimeInstallPolicy: 'auto',
|
||||
},
|
||||
io.io,
|
||||
{ runLocalScan, createLocalIngestAdapters },
|
||||
{ runLocalScan, createLocalIngestAdapters, runtimeIo: runtimeIo.io } as KtxScanDeps & {
|
||||
runtimeIo: typeof runtimeIo.io;
|
||||
},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
|
|
@ -415,7 +418,7 @@ describe('runKtxScan', () => {
|
|||
cliVersion: '0.2.0',
|
||||
projectDir: tempDir,
|
||||
installPolicy: 'auto',
|
||||
io: io.io,
|
||||
io: runtimeIo.io,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface KtxScanDeps {
|
|||
runLocalScan?: typeof runLocalScan;
|
||||
createLocalIngestAdapters?: typeof createKtxCliLocalIngestAdapters;
|
||||
progress?: KtxProgressPort;
|
||||
runtimeIo?: KtxCliIo;
|
||||
}
|
||||
|
||||
function shouldUseStyledOutput(io: KtxCliIo): boolean {
|
||||
|
|
@ -313,7 +314,7 @@ export function createCliScanProgress(
|
|||
export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps: KtxScanDeps = {}): Promise<number> {
|
||||
try {
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
const managedDaemon = managedDaemonOptionsForScanRun(args, io);
|
||||
const managedDaemon = managedDaemonOptionsForScanRun(args, deps.runtimeIo ?? io);
|
||||
const connector =
|
||||
args.mode !== 'structural' || args.detectRelationships
|
||||
? await createKtxCliScanConnector(project, args.connectionId)
|
||||
|
|
|
|||
|
|
@ -1051,6 +1051,53 @@ describe('setup status', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('auto-installs the managed runtime by default during setup', async () => {
|
||||
const io = makeIo();
|
||||
const embeddings = vi.fn(async () => ({ status: 'ready' as const, projectDir: tempDir }));
|
||||
const context = vi.fn(async () => ({ status: 'failed' as const, projectDir: tempDir }));
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: tempDir,
|
||||
mode: 'new',
|
||||
agents: false,
|
||||
agentScope: 'project',
|
||||
skipAgents: true,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
cliVersion: '0.2.0',
|
||||
skipLlm: true,
|
||||
skipEmbeddings: false,
|
||||
databaseSchemas: [],
|
||||
skipDatabases: true,
|
||||
skipSources: true,
|
||||
},
|
||||
io.io,
|
||||
{
|
||||
embeddings,
|
||||
context,
|
||||
},
|
||||
),
|
||||
).resolves.toBe(1);
|
||||
|
||||
expect(embeddings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cliVersion: '0.2.0',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
}),
|
||||
io.io,
|
||||
);
|
||||
expect(context).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cliVersion: '0.2.0',
|
||||
runtimeInstallPolicy: 'auto',
|
||||
}),
|
||||
io.io,
|
||||
);
|
||||
});
|
||||
|
||||
it('lets Back from embedding setup return to the model step instead of exiting', async () => {
|
||||
const testIo = makeIo();
|
||||
const modelResults = [
|
||||
|
|
|
|||
|
|
@ -412,10 +412,7 @@ function writeContextNotReadyForAgents(projectDir: string, io: KtxCliIo): void {
|
|||
}
|
||||
|
||||
function setupRuntimeInstallPolicy(args: Extract<KtxSetupArgs, { command: 'run' }>): 'prompt' | 'auto' | 'never' {
|
||||
if (args.yes) {
|
||||
return 'auto';
|
||||
}
|
||||
return args.inputMode === 'disabled' ? 'never' : 'prompt';
|
||||
return args.inputMode === 'disabled' && !args.yes ? 'never' : 'auto';
|
||||
}
|
||||
|
||||
async function commitSetupConfigChanges(projectDir: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ describe('standalone example docs', () => {
|
|||
const quickstart = await readText('docs-site/content/docs/getting-started/quickstart.mdx');
|
||||
const packageArtifacts = await readText('examples/package-artifacts/README.md');
|
||||
|
||||
assert.match(rootReadme, publicPackagePattern('npm install -g {package}'));
|
||||
assert.match(rootReadme, publicPackagePattern('pnpm add --global {package}'));
|
||||
assert.match(quickstart, publicPackagePattern('npm install -g {package}'));
|
||||
assert.match(quickstart, /ktx dev runtime install --feature local-embeddings --yes/);
|
||||
assert.match(quickstart, /ktx dev runtime start --feature local-embeddings/);
|
||||
|
|
@ -261,7 +261,7 @@ describe('standalone example docs', () => {
|
|||
assert.match(contextAsCode, /ktx ingest --all --no-input/);
|
||||
assert.match(quickstart, /schema context/);
|
||||
assert.match(primarySources, /context:\n queryHistory:/);
|
||||
assert.match(rootReadme, /Databases configured: yes \(postgres-warehouse\)/);
|
||||
assert.match(rootReadme, /`ktx ingest <id>` \| Build context for one connection/);
|
||||
assert.match(quickstart, /Databases:\n warehouse: deep context complete/);
|
||||
assert.match(quickstart, /Databases configured: yes \(warehouse\)/);
|
||||
assert.match(setupReference, /Databases configured: yes \(postgres-warehouse\)/);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue