fix(cli): align setup database labels

This commit is contained in:
Andrey Avtomonov 2026-05-13 21:07:32 +02:00
parent 869669e87f
commit 38e921d3e2
12 changed files with 105 additions and 63 deletions

View file

@ -65,7 +65,7 @@ KTX project: /home/user/analytics
Project ready: yes
LLM ready: yes (claude-sonnet-4-6)
Embeddings ready: yes (text-embedding-3-small)
Primary sources configured: yes (postgres-warehouse)
Databases configured: yes (postgres-warehouse)
Context sources configured: yes (dbt-main)
KTX context built: yes
Agent integration ready: yes (claude-code:project)

View file

@ -143,7 +143,7 @@ KTX project: /home/user/analytics
Project ready: yes
LLM ready: yes (claude-sonnet-4-6)
Embeddings ready: yes (text-embedding-3-small)
Primary sources configured: yes (postgres-warehouse)
Databases configured: yes (postgres-warehouse)
Context sources configured: yes (dbt-main)
KTX context built: yes
Agent integration ready: yes (codex:project)

View file

@ -108,7 +108,7 @@ Building schema context for postgres-warehouse
Schema context complete for postgres-warehouse
Changes: 42 new tables
Primary source ready
Database ready
postgres-warehouse - PostgreSQL - schema context complete
```
@ -167,7 +167,7 @@ When the build completes, KTX verifies that agent-ready context was produced:
```
KTX context is ready for agents.
Primary sources:
Databases:
postgres-warehouse: deep context complete
Context sources:
@ -228,7 +228,7 @@ KTX project: /home/user/analytics
Project ready: yes
LLM ready: yes (claude-sonnet-4-6)
Embeddings ready: yes (text-embedding-3-small)
Primary sources configured: yes (postgres-warehouse)
Databases configured: yes (postgres-warehouse)
Context sources configured: yes (dbt-main)
KTX context built: yes
Agent integration ready: yes (claude-code:project)

View file

@ -293,6 +293,8 @@ describe('setup context build state', () => {
artifactPaths: ['raw-sources/warehouse/live-database/sync-1/scan-report.json'],
});
expect(io.stdout()).toContain('KTX context is ready for agents.');
expect(io.stdout()).toContain('Databases:');
expect(io.stdout()).not.toContain(['Primary sources', ':'].join(''));
});
it('records only failed sources as retryable when the context build fails', async () => {
@ -375,6 +377,7 @@ describe('setup context build state', () => {
contextSourceConnectionIds: ['docs'],
});
expect(io.stdout()).toContain('KTX context is ready for agents.');
expect(io.stdout()).not.toContain(['Primary sources', ':'].join(''));
});
it('does not mark context ready until primary scans have completed description enrichment', async () => {

View file

@ -570,7 +570,7 @@ function writeSuccess(
io: KtxCliIo,
): void {
io.stdout.write('\nKTX context is ready for agents.\n\n');
io.stdout.write('Primary sources:\n');
io.stdout.write('Databases:\n');
if (targets.primarySourceConnectionIds.length === 0) {
io.stdout.write(' none\n');
} else {

View file

@ -76,7 +76,7 @@ describe('setup databases step', () => {
await rm(tempDir, { recursive: true, force: true });
});
it('shows every supported primary source in the interactive checklist', async () => {
it('shows every supported database in the interactive checklist', async () => {
const prompts = makePromptAdapter({ multiselectValues: [['back']] });
const result = await runKtxSetupDatabasesStep(
@ -88,7 +88,7 @@ describe('setup databases step', () => {
expect(result.status).toBe('back');
expect(prompts.multiselect).toHaveBeenCalledWith({
message:
'Which primary sources should KTX connect to?\n' +
'Which databases should KTX connect to?\n' +
'Use Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
options: [
{ value: 'sqlite', label: 'SQLite' },
@ -103,7 +103,7 @@ describe('setup databases step', () => {
});
});
it('requires choosing a primary source after an empty interactive selection', async () => {
it('requires choosing a database after an empty interactive selection', async () => {
const io = makeIo();
const prompts = makePromptAdapter({
multiselectValues: [[], ['back']],
@ -119,12 +119,12 @@ describe('setup databases step', () => {
expect(result.status).toBe('back');
expect(prompts.select).not.toHaveBeenCalled();
expect(io.stdout()).toContain(
'KTX cannot work without at least one primary source. Select a source or press Escape to go back.',
'KTX cannot work without at least one database. Select a database or press Escape to go back.',
);
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
});
it('lets Back from connection method selection return to primary source selection when adding a new driver', async () => {
it('lets Back from connection method selection return to database selection when adding a new driver', async () => {
const prompts = makePromptAdapter({
multiselectValues: [['postgres'], ['back']],
selectValues: ['back'],
@ -147,12 +147,12 @@ describe('setup databases step', () => {
});
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
expect(vi.mocked(prompts.multiselect).mock.calls[1]?.[0].message).toBe(
'Which primary sources should KTX connect to?\n' +
'Which databases should KTX connect to?\n' +
'Use Up/Down to move, Space to select or unselect, Enter to confirm, Escape to go back, or Ctrl+C to exit.',
);
});
it('offers connection URL paste first for URL-capable primary sources', async () => {
it('offers connection URL paste first for URL-capable databases', async () => {
const cases: Array<{ driver: KtxSetupDatabaseDriver; label: string }> = [
{ driver: 'postgres', label: 'PostgreSQL' },
{ driver: 'mysql', label: 'MySQL' },
@ -505,7 +505,7 @@ describe('setup databases step', () => {
}
});
it('lets Back from connection method selection return to primary source selection', async () => {
it('lets Back from connection method selection return to database selection', async () => {
const prompts = makePromptAdapter({
multiselectValues: [['postgres'], ['back']],
selectValues: ['back'],
@ -542,7 +542,7 @@ describe('setup databases step', () => {
expect(scanConnection).not.toHaveBeenCalled();
});
it('shows a configured primary source menu instead of the type checklist when a primary source exists', async () => {
it('shows a configured database menu instead of the type checklist when a database exists', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
[
@ -565,7 +565,13 @@ describe('setup databases step', () => {
const scanConnection = vi.fn(async () => 0);
const result = await runKtxSetupDatabasesStep(
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
{
projectDir: tempDir,
inputMode: 'auto',
skipDatabases: false,
databaseSchemas: [],
disableQueryHistory: true,
},
makeIo().io,
{ prompts, testConnection, scanConnection },
);
@ -573,17 +579,17 @@ describe('setup databases step', () => {
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
expect(prompts.multiselect).not.toHaveBeenCalled();
expect(prompts.select).toHaveBeenCalledWith({
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
message: 'Databases already configured: warehouse\nWhat would you like to do?',
options: [
{ value: 'continue', label: 'Continue to knowledge sources' },
{ value: 'add', label: 'Add another primary source' },
{ value: 'continue', label: 'Continue to context sources' },
{ value: 'add', label: 'Add another database' },
],
});
expect(testConnection).not.toHaveBeenCalled();
expect(scanConnection).not.toHaveBeenCalled();
});
it('preserves existing primary source ids when adding another source from the configured menu', async () => {
it('preserves existing database ids when adding another database from the configured menu', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
[
@ -610,7 +616,13 @@ describe('setup databases step', () => {
const scanConnection = vi.fn(async () => 0);
const result = await runKtxSetupDatabasesStep(
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
{
projectDir: tempDir,
inputMode: 'auto',
skipDatabases: false,
databaseSchemas: [],
disableQueryHistory: true,
},
makeIo().io,
{ prompts, testConnection, scanConnection },
);
@ -622,10 +634,10 @@ describe('setup databases step', () => {
});
expect(prompts.multiselect).toHaveBeenCalledTimes(1);
expect(prompts.select).toHaveBeenCalledWith({
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
message: 'Databases already configured: warehouse\nWhat would you like to do?',
options: [
{ value: 'continue', label: 'Continue to knowledge sources' },
{ value: 'add', label: 'Add another primary source' },
{ value: 'continue', label: 'Continue to context sources' },
{ value: 'add', label: 'Add another database' },
],
});
expect(testConnection).toHaveBeenCalledTimes(1);
@ -635,7 +647,7 @@ describe('setup databases step', () => {
expect(config.setup?.database_connection_ids).toEqual(['warehouse', 'mysql-warehouse']);
});
it('lets users add another primary source after completing the first one', async () => {
it('lets users add another database after completing the first one', async () => {
const prompts = makePromptAdapter({
multiselectValues: [['postgres'], ['mysql']],
selectValues: ['url', 'add', 'url', 'continue'],
@ -645,7 +657,13 @@ describe('setup databases step', () => {
const scanConnection = vi.fn(async () => 0);
const result = await runKtxSetupDatabasesStep(
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
{
projectDir: tempDir,
inputMode: 'auto',
skipDatabases: false,
databaseSchemas: [],
disableQueryHistory: true,
},
makeIo().io,
{ prompts, testConnection, scanConnection },
);
@ -657,10 +675,10 @@ describe('setup databases step', () => {
});
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
expect(prompts.select).toHaveBeenCalledWith({
message: 'Primary sources already configured: postgres-warehouse\nWhat would you like to do?',
message: 'Databases already configured: postgres-warehouse\nWhat would you like to do?',
options: [
{ value: 'continue', label: 'Continue to knowledge sources' },
{ value: 'add', label: 'Add another primary source' },
{ value: 'continue', label: 'Continue to context sources' },
{ value: 'add', label: 'Add another database' },
],
});
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
@ -678,7 +696,13 @@ describe('setup databases step', () => {
const scanConnection = vi.fn(async () => 0);
const result = await runKtxSetupDatabasesStep(
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
{
projectDir: tempDir,
inputMode: 'auto',
skipDatabases: false,
databaseSchemas: [],
disableQueryHistory: true,
},
io.io,
{ prompts, testConnection, scanConnection },
);
@ -689,12 +713,12 @@ describe('setup databases step', () => {
connectionIds: ['postgres-warehouse'],
});
expect(prompts.multiselect).toHaveBeenCalledTimes(2);
expect(io.stdout()).not.toContain('KTX cannot work without at least one primary source');
expect(io.stdout()).not.toContain('KTX cannot work without at least one database');
expect(prompts.select).toHaveBeenNthCalledWith(2, {
message: 'Primary sources already configured: postgres-warehouse\nWhat would you like to do?',
message: 'Databases already configured: postgres-warehouse\nWhat would you like to do?',
options: [
{ value: 'continue', label: 'Continue to knowledge sources' },
{ value: 'add', label: 'Add another primary source' },
{ value: 'continue', label: 'Continue to context sources' },
{ value: 'add', label: 'Add another database' },
],
});
});
@ -730,12 +754,12 @@ describe('setup databases step', () => {
);
expect(result).toEqual({ status: 'ready', projectDir: tempDir, connectionIds: ['warehouse'] });
expect(io.stdout()).not.toContain('KTX cannot work without at least one primary source');
expect(io.stdout()).not.toContain('KTX cannot work without at least one database');
expect(prompts.select).toHaveBeenNthCalledWith(2, {
message: 'Primary sources already configured: warehouse\nWhat would you like to do?',
message: 'Databases already configured: warehouse\nWhat would you like to do?',
options: [
{ value: 'continue', label: 'Continue to knowledge sources' },
{ value: 'add', label: 'Add another primary source' },
{ value: 'continue', label: 'Continue to context sources' },
{ value: 'add', label: 'Add another database' },
],
});
});
@ -755,6 +779,7 @@ describe('setup databases step', () => {
databaseDrivers: ['postgres'],
databaseSchemas: [],
skipDatabases: false,
disableQueryHistory: true,
},
makeIo().io,
{ prompts, testConnection, scanConnection },
@ -788,15 +813,15 @@ describe('setup databases step', () => {
expect(prompts.select).toHaveBeenNthCalledWith(2, {
message:
'Some PostgreSQL connection details are missing.\n' +
'Continue entering details, or go back to primary source selection.',
'Continue entering details, or go back to database selection.',
options: [
{ value: 'retry', label: 'Continue entering PostgreSQL details' },
{ value: 'back', label: 'Back to primary source selection' },
{ value: 'back', label: 'Back to database selection' },
],
});
});
it('lets Escape from connection name return to primary source selection', async () => {
it('lets Escape from connection name return to database selection', async () => {
const prompts = makePromptAdapter({
multiselectValues: [['postgres'], ['back']],
textValues: [undefined],
@ -966,7 +991,8 @@ describe('setup databases step', () => {
expect(io.stdout()).toContain('│ Running fast database ingest…');
expect(io.stdout()).toContain('◇ Schema context complete for postgres-warehouse');
expect(io.stdout()).toContain('│ Changes: 2 new tables');
expect(io.stdout()).toContain('◇ Primary source ready');
expect(io.stdout()).toContain('◇ Database ready');
expect(io.stdout()).not.toContain(['Primary source', 'ready'].join(' '));
expect(io.stdout()).toContain('│ postgres-warehouse · PostgreSQL · schema context complete');
expect(io.stdout()).not.toContain('Scanning postgres-warehouse');
expect(io.stdout()).not.toContain('Scan complete for postgres-warehouse');
@ -1108,6 +1134,7 @@ describe('setup databases step', () => {
databaseUrl: 'env:DATABASE_URL',
databaseSchemas: ['public'],
skipDatabases: false,
disableQueryHistory: true,
},
io.io,
{ testConnection, scanConnection, listSchemas },
@ -1122,13 +1149,15 @@ describe('setup databases step', () => {
driver: 'postgres',
url: 'env:DATABASE_URL',
schemas: ['public'],
context: { queryHistory: { enabled: false } },
readonly: true,
});
expect(config.setup).toEqual({
database_connection_ids: ['warehouse'],
});
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('databases');
expect(io.stdout()).toContain('Primary source ready');
expect(io.stdout()).toContain('Database ready');
expect(io.stdout()).not.toContain(['Primary source', 'ready'].join(' '));
expect(io.stdout()).not.toContain('DATABASE_URL=');
});
@ -1813,7 +1842,7 @@ describe('setup databases step', () => {
expect(io.stderr()).toContain('"replay" is reserved for ktx ingest replay; choose a different connection id.');
});
it('leaves setup incomplete when primary sources are skipped', async () => {
it('leaves setup incomplete when databases are skipped', async () => {
const io = makeIo();
const result = await runKtxSetupDatabasesStep(
@ -1822,7 +1851,7 @@ describe('setup databases step', () => {
);
expect(result.status).toBe('skipped');
expect(io.stdout()).toContain('KTX cannot work until you add a primary source.');
expect(io.stdout()).toContain('KTX cannot work until you add a database.');
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
});
});

View file

@ -197,7 +197,7 @@ function missingConnectionDetailsPrompt(
label: string,
canReturnToDriverSelection: boolean,
): { message: string; options: Array<{ value: string; label: string }> } {
const backDestination = canReturnToDriverSelection ? 'primary source selection' : 'the previous setup step';
const backDestination = canReturnToDriverSelection ? 'database selection' : 'the previous setup step';
return {
message:
`Some ${label} connection details are missing.\n` +
@ -509,10 +509,10 @@ function configuredPrimarySourcesPrompt(connectionIds: string[]): {
options: Array<{ value: string; label: string }>;
} {
return {
message: `Primary sources already configured: ${connectionIds.join(', ')}\nWhat would you like to do?`,
message: `Databases already configured: ${connectionIds.join(', ')}\nWhat would you like to do?`,
options: [
{ value: 'continue', label: 'Continue to knowledge sources' },
{ value: 'add', label: 'Add another primary source' },
{ value: 'continue', label: 'Continue to context sources' },
{ value: 'add', label: 'Add another database' },
],
};
}
@ -1609,7 +1609,7 @@ async function validateAndScanConnection(input: {
`Schema context complete for ${input.connectionId}`,
[`Changes: ${summarizeScanChanges(scanOutput)}`],
);
writeSetupSection(input.io, 'Primary source ready', [
writeSetupSection(input.io, 'Database ready', [
`${input.connectionId} · ${driverDisplay} · schema context complete`,
]);
return true;
@ -1629,13 +1629,13 @@ async function chooseDrivers(
}
if (args.inputMode === 'disabled') {
io.stderr.write(
'KTX cannot work without a primary source. Pass --database or --database-connection-id, or pass --skip-databases to leave setup incomplete.\n',
'KTX cannot work without a database. Pass --database or --database-connection-id, or pass --skip-databases to leave setup incomplete.\n',
);
return 'missing-input';
}
while (true) {
const choices = await prompts.multiselect({
message: withMultiselectNavigation('Which primary sources should KTX connect to?'),
message: withMultiselectNavigation('Which databases should KTX connect to?'),
options: [...DRIVER_OPTIONS],
required: false,
});
@ -1650,7 +1650,7 @@ async function chooseDrivers(
return 'back';
}
io.stdout.write('│ KTX cannot work without at least one primary source. Select a source or press Escape to go back.\n');
io.stdout.write('│ KTX cannot work without at least one database. Select a database or press Escape to go back.\n');
}
}
@ -1718,7 +1718,7 @@ export async function runKtxSetupDatabasesStep(
deps: KtxSetupDatabasesDeps = {},
): Promise<KtxSetupDatabasesResult> {
if (args.skipDatabases) {
io.stdout.write('│ Primary source setup skipped. KTX cannot work until you add a primary source.\n');
io.stdout.write('│ Database setup skipped. KTX cannot work until you add a database.\n');
return { status: 'skipped', projectDir: args.projectDir };
}
@ -1772,7 +1772,7 @@ export async function runKtxSetupDatabasesStep(
if (drivers === 'missing-input') return { status: 'missing-input', projectDir: args.projectDir };
if (drivers.length === 0) {
await markDatabasesComplete(args.projectDir, []);
io.stdout.write('│ KTX cannot work without a primary source.\n');
io.stdout.write('│ KTX cannot work without a database.\n');
return { status: 'skipped', projectDir: args.projectDir };
}
@ -1883,11 +1883,11 @@ export async function runKtxSetupDatabasesStep(
) {
if (args.inputMode === 'disabled') return { status: 'failed', projectDir: args.projectDir };
const action = await prompts.select({
message: `Primary source setup failed for ${connectionChoice.connectionId}`,
message: `Database setup failed for ${connectionChoice.connectionId}`,
options: [
{ value: 'retry', label: 'Retry connection test' },
{ value: 're-enter', label: 'Re-enter connection details' },
{ value: 'skip', label: 'Skip this primary source' },
{ value: 'skip', label: 'Skip this database' },
{ value: 'back', label: 'Back' },
],
});
@ -1940,7 +1940,7 @@ export async function runKtxSetupDatabasesStep(
}
if (selectedConnectionIds.length === 0) {
io.stderr.write('No primary source connections completed setup.\n');
io.stderr.write('No database connections completed setup.\n');
return { status: 'failed', projectDir: args.projectDir };
}

View file

@ -37,7 +37,7 @@ describe('setup ready menu', () => {
options: [
{ value: 'models', label: 'Models' },
{ value: 'embeddings', label: 'Embeddings' },
{ value: 'databases', label: 'Primary sources' },
{ value: 'databases', label: 'Databases' },
{ value: 'sources', label: 'Context sources' },
{ value: 'context', label: 'Rebuild KTX context' },
{ value: 'agents', label: 'Agent integration' },

View file

@ -44,7 +44,7 @@ export async function runKtxSetupReadyChangeMenu(
options: [
{ value: 'models', label: 'Models' },
{ value: 'embeddings', label: 'Embeddings' },
{ value: 'databases', label: 'Primary sources' },
{ value: 'databases', label: 'Databases' },
{ value: 'sources', label: 'Context sources' },
{ value: 'context', label: 'Rebuild KTX context' },
{ value: 'agents', label: 'Agent integration' },

View file

@ -379,6 +379,8 @@ describe('setup status', () => {
expect(rendered).toContain(`KTX project: ${tempDir}`);
expect(rendered).toContain('Project ready: yes');
expect(rendered).toContain('LLM ready: no');
expect(rendered).toContain('Databases configured: no');
expect(rendered).not.toContain(['Primary sources', 'configured'].join(' '));
expect(rendered).toContain('KTX context built: no');
expect(rendered).not.toContain('No KTX project found.');
});
@ -1143,11 +1145,11 @@ describe('setup status', () => {
expect(databasePrompts.select).not.toHaveBeenCalled();
expect(testIo.stdout()).toContain(
'KTX cannot work without at least one primary source. Select a source or press Escape to go back.',
'KTX cannot work without at least one database. Select a database or press Escape to go back.',
);
expect(embeddings).toHaveBeenCalledTimes(2);
expect(embeddings).toHaveBeenNthCalledWith(2, expect.objectContaining({ forcePrompt: true }), testIo.io);
expect(testIo.stderr()).not.toContain('No primary sources selected.');
expect(testIo.stderr()).not.toContain('No databases selected.');
});
it('lets Back from the first setup step return to the entry menu instead of exiting', async () => {

View file

@ -371,7 +371,7 @@ export function formatKtxSetupStatus(status: KtxSetupStatus): string {
`Embeddings ready: ${formatReady(status.embeddings.ready)}${
status.embeddings.model ? ` (${status.embeddings.model})` : ''
}`,
`Primary sources configured: ${formatConnectionList(status.databases.map((database) => database.connectionId))}`,
`Databases configured: ${formatConnectionList(status.databases.map((database) => database.connectionId))}`,
`Context sources configured: ${formatConnectionList(status.sources.map((source) => source.connectionId))}`,
`KTX context built: ${formatContextBuilt(status.context)}`,
`Agent integration ready: ${formatReady(status.agents.some((agent) => agent.ready))}${

View file

@ -243,6 +243,7 @@ describe('standalone example docs', () => {
const cliMeta = await readText('docs-site/content/docs/cli-reference/meta.json');
const ingestReference = await readText('docs-site/content/docs/cli-reference/ktx-ingest.mdx');
const devReference = await readText('docs-site/content/docs/cli-reference/ktx-dev.mdx');
const setupReference = await readText('docs-site/content/docs/cli-reference/ktx-setup.mdx');
const buildingContext = await readText('docs-site/content/docs/guides/building-context.mdx');
const contextSources = await readText('docs-site/content/docs/integrations/context-sources.mdx');
const contextAsCode = await readText('docs-site/content/docs/concepts/context-as-code.mdx');
@ -261,6 +262,13 @@ 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(quickstart, /Databases:\n postgres-warehouse: deep context complete/);
assert.match(quickstart, /Databases configured: yes \(postgres-warehouse\)/);
assert.match(setupReference, /Databases configured: yes \(postgres-warehouse\)/);
assert.doesNotMatch(rootReadme, new RegExp(['Primary sources', 'configured'].join(' ')));
assert.doesNotMatch(quickstart, new RegExp(['Primary', 'sources'].join(' ')));
assert.doesNotMatch(setupReference, new RegExp(['Primary sources', 'configured'].join(' ')));
assert.doesNotMatch(cliMeta, /ktx-scan/);
assert.doesNotMatch(ingestReference, /ktx ingest run/);