mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
feat(cli): guide next action at end of ktx setup, not reruns (#256)
Re-running setup was the dominant action for installs that completed setup but never ingested. Classify completion (incomplete | needs-context | needs-agents | ready) and drive one obvious next action per state: route a config-complete project straight to the build, point unbuilt-context users at `ktx ingest` instead of re-running setup or dropping to a bare shell, and confirm readiness for fully-set-up projects rather than reopening the edit menu.
This commit is contained in:
parent
cb6a67c2d7
commit
45aa95d2cc
8 changed files with 360 additions and 59 deletions
|
|
@ -215,8 +215,8 @@ The wizard walks you through everything **ktx** needs in one pass:
|
|||
SQLite, PostgreSQL, MySQL, SQL Server, BigQuery, and Snowflake.
|
||||
5. **Context sources** - optionally adds dbt, MetricFlow, LookML, Looker,
|
||||
Metabase, or Notion. You can skip and add them later.
|
||||
6. **Build** - runs the first ingest so semantic sources and wiki pages
|
||||
are ready for agents.
|
||||
6. **Build** - offers to run the first ingest so semantic sources and wiki
|
||||
pages are ready for agents. If you skip it, build later with `ktx ingest`.
|
||||
7. **Agent integration** - installs project-local rules for Claude Code,
|
||||
Codex, Cursor, OpenCode, or universal `.agents`.
|
||||
|
||||
|
|
@ -247,6 +247,18 @@ progress under `.ktx/setup/` and resumes from the remaining work.
|
|||
> resuming setup, connecting an agent, checking status, or exploring a
|
||||
> pre-built demo project.
|
||||
|
||||
When the wizard finishes, it states where you stand and the single next action:
|
||||
|
||||
- **Context built** - **ktx** confirms it is ready for agents and points you to
|
||||
open your coding agent and ask a data question.
|
||||
- **Build skipped** - **ktx** tells you setup is complete and that the only step
|
||||
left is to build context with `ktx ingest`.
|
||||
|
||||
Re-running `ktx setup` on an already-configured project goes straight to the
|
||||
remaining step - building context or connecting an agent - instead of
|
||||
re-asking every question. Once everything is ready, it confirms you are set
|
||||
rather than reopening the configuration menu.
|
||||
|
||||
## Verify
|
||||
|
||||
When setup finishes, check readiness:
|
||||
|
|
@ -268,6 +280,9 @@ Agent integration ready: yes (codex:project)
|
|||
|
||||
For a structured check inside scripts, use `ktx status --json`.
|
||||
|
||||
If you skipped the build, `ktx context built` shows `no`. Build it with
|
||||
`ktx ingest` - there is no need to re-run `ktx setup`.
|
||||
|
||||
When setup finishes building context, its final context check looks like:
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -70,8 +70,7 @@ export function formatSetupNextStepLines(state: KtxSetupNextStepState, indent =
|
|||
|
||||
if (!state.contextReady) {
|
||||
return [
|
||||
`${indent}Build KTX context next.`,
|
||||
`${indent}Run ingest to build database schema context before context-source ingest.`,
|
||||
`${indent}Setup is complete. The only step left is to build context for your agents.`,
|
||||
...commandLines(KTX_CONTEXT_BUILD_COMMANDS, indent),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,12 +441,10 @@ function writeMissingCapabilities(missing: string[], io: KtxCliIo): void {
|
|||
io.stderr.write('\nFix this in setup before building context.\n');
|
||||
}
|
||||
|
||||
function writeSkippedContext(projectDir: string, io: KtxCliIo): void {
|
||||
io.stdout.write('\nKTX is configured, but context has not been built yet.\n\n');
|
||||
io.stdout.write('Agents were not connected because KTX has not prepared searchable context for them.\n\n');
|
||||
io.stdout.write(`Resume setup:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
|
||||
io.stdout.write(`Build context:\n ktx setup --project-dir ${resolve(projectDir)}\n\n`);
|
||||
io.stdout.write(`Check status:\n ktx status --project-dir ${resolve(projectDir)}\n`);
|
||||
function writeSkippedContext(io: KtxCliIo): void {
|
||||
// The setup completion screen owns "what to do next" (it points at `ktx ingest`),
|
||||
// so keep this to a short acknowledgement rather than a competing command list.
|
||||
io.stdout.write('\nLeaving context unbuilt for now.\n');
|
||||
}
|
||||
|
||||
function writeSuccess(
|
||||
|
|
@ -695,7 +693,7 @@ export async function runKtxSetupContextStep(
|
|||
return { status: 'back', projectDir: args.projectDir };
|
||||
}
|
||||
if (choice === 'skip') {
|
||||
writeSkippedContext(args.projectDir, io);
|
||||
writeSkippedContext(io);
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ export type KtxSetupReadyAction =
|
|||
| 'agents'
|
||||
| 'exit';
|
||||
|
||||
/**
|
||||
* Where a project stands once its `ktx.yaml` exists. Single source of truth for the
|
||||
* end-of-setup interception: each state maps to exactly one obvious next action.
|
||||
*/
|
||||
export type KtxSetupCompletion = 'incomplete' | 'needs-context' | 'needs-agents' | 'ready';
|
||||
|
||||
interface KtxSetupReadyMenuPromptAdapter {
|
||||
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
|
||||
cancel(message: string): void;
|
||||
|
|
@ -23,7 +29,11 @@ export interface KtxSetupReadyMenuDeps {
|
|||
prompts?: KtxSetupReadyMenuPromptAdapter;
|
||||
}
|
||||
|
||||
export function isKtxPreAgentSetupReady(status: KtxSetupStatus): boolean {
|
||||
export function setupHasContextTargets(status: KtxSetupStatus): boolean {
|
||||
return status.databases.length > 0 || status.sources.length > 0;
|
||||
}
|
||||
|
||||
function setupConfigReady(status: KtxSetupStatus): boolean {
|
||||
return (
|
||||
status.project.ready &&
|
||||
status.llm.ready &&
|
||||
|
|
@ -31,25 +41,58 @@ export function isKtxPreAgentSetupReady(status: KtxSetupStatus): boolean {
|
|||
status.databases.every((database) => database.ready) &&
|
||||
status.sources.every((source) => source.ready) &&
|
||||
status.runtime.ready &&
|
||||
status.context.ready
|
||||
setupHasContextTargets(status)
|
||||
);
|
||||
}
|
||||
|
||||
export function isKtxSetupReady(status: KtxSetupStatus): boolean {
|
||||
return isKtxPreAgentSetupReady(status) && status.agents.some((agent) => agent.ready);
|
||||
export function classifyKtxSetupCompletion(status: KtxSetupStatus): KtxSetupCompletion {
|
||||
if (!setupConfigReady(status)) {
|
||||
return 'incomplete';
|
||||
}
|
||||
if (!status.context.ready) {
|
||||
return 'needs-context';
|
||||
}
|
||||
if (!status.agents.some((agent) => agent.ready)) {
|
||||
return 'needs-agents';
|
||||
}
|
||||
return 'ready';
|
||||
}
|
||||
|
||||
function createPromptAdapter(): KtxSetupReadyMenuPromptAdapter {
|
||||
return createKtxSetupPromptAdapter({ selectCancelValue: 'exit' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown when a returning user re-runs `ktx setup` on a fully-ready project. Leads with
|
||||
* "you're done" (the readiness note is printed by the caller first) and keeps the
|
||||
* section editor one explicit step away rather than defaulting into it.
|
||||
*/
|
||||
export async function runKtxSetupReadyMenu(
|
||||
status: KtxSetupStatus,
|
||||
deps: KtxSetupReadyMenuDeps = {},
|
||||
): Promise<{ action: KtxSetupReadyAction }> {
|
||||
const prompts = deps.prompts ?? createPromptAdapter();
|
||||
const choice = await prompts.select({
|
||||
message: 'Anything else?',
|
||||
options: [
|
||||
{ value: 'done', label: "Done — I'll start using ktx" },
|
||||
{ value: 'change', label: 'Change a setting' },
|
||||
],
|
||||
});
|
||||
if (choice !== 'change') {
|
||||
return { action: 'exit' };
|
||||
}
|
||||
return runKtxSetupReadyChangeMenu(status, { prompts });
|
||||
}
|
||||
|
||||
/** @internal Reached only through {@link runKtxSetupReadyMenu}; exported for unit tests. */
|
||||
export async function runKtxSetupReadyChangeMenu(
|
||||
status: KtxSetupStatus,
|
||||
deps: KtxSetupReadyMenuDeps = {},
|
||||
): Promise<{ action: KtxSetupReadyAction }> {
|
||||
const prompts = deps.prompts ?? createPromptAdapter();
|
||||
const action = (await prompts.select({
|
||||
message: `KTX is already set up for ${status.project.name ?? status.project.path}. What would you like to change?`,
|
||||
message: 'What would you like to change?',
|
||||
options: [
|
||||
{ value: 'models', label: 'Models' },
|
||||
{ value: 'embeddings', label: 'Embeddings' },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
|
|||
import { loadKtxProject, type KtxLocalProject } from './context/project/project.js';
|
||||
import { readKtxSetupState } from './context/project/setup-config.js';
|
||||
import { getKtxCliPackageInfo, type KtxCliIo } from './cli-runtime.js';
|
||||
import { formatSetupNextStepLines } from './next-steps.js';
|
||||
import { formatNextStepLines, formatSetupNextStepLines } from './next-steps.js';
|
||||
import { runtimeInstallPolicyFromFlags } from './managed-python-command.js';
|
||||
import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
|
||||
import { resolveProjectRuntimeRequirements } from './runtime-requirements.js';
|
||||
|
|
@ -33,10 +33,10 @@ import {
|
|||
} from './setup-models.js';
|
||||
import { type KtxSetupProjectDeps, runKtxSetupProjectStep } from './setup-project.js';
|
||||
import {
|
||||
isKtxPreAgentSetupReady,
|
||||
isKtxSetupReady,
|
||||
classifyKtxSetupCompletion,
|
||||
type KtxSetupReadyMenuDeps,
|
||||
runKtxSetupReadyChangeMenu,
|
||||
runKtxSetupReadyMenu,
|
||||
setupHasContextTargets,
|
||||
} from './setup-ready-menu.js';
|
||||
import { type KtxSetupSourcesDeps, type KtxSetupSourceType, runKtxSetupSourcesStep } from './setup-sources.js';
|
||||
import {
|
||||
|
|
@ -529,10 +529,6 @@ function setupStatusReady(status: KtxSetupStatus): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function setupHasContextTargets(status: KtxSetupStatus): boolean {
|
||||
return status.databases.length > 0 || status.sources.length > 0;
|
||||
}
|
||||
|
||||
function setupContextReady(status: KtxSetupStatus): boolean {
|
||||
return status.context.ready;
|
||||
}
|
||||
|
|
@ -630,12 +626,19 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
|
|||
let readyAction: string | undefined;
|
||||
|
||||
if (args.inputMode !== 'disabled' && !agentsRequested) {
|
||||
if (isKtxSetupReady(currentStatus)) {
|
||||
readyAction = (await runKtxSetupReadyChangeMenu(currentStatus, deps.readyMenuDeps)).action;
|
||||
if (readyAction === 'exit') return 0;
|
||||
} else if (isKtxPreAgentSetupReady(currentStatus)) {
|
||||
const completion = classifyKtxSetupCompletion(currentStatus);
|
||||
if (completion === 'ready') {
|
||||
setupUi.note(formatNextStepLines().join('\n'), 'ktx is ready', io);
|
||||
const choice = (await runKtxSetupReadyMenu(currentStatus, deps.readyMenuDeps)).action;
|
||||
if (choice === 'exit') return 0;
|
||||
readyAction = choice;
|
||||
} else if (completion === 'needs-context') {
|
||||
// Config is done; skip the re-walk and land straight on the build prompt.
|
||||
readyAction = 'context';
|
||||
} else if (completion === 'needs-agents') {
|
||||
readyAction = 'agents';
|
||||
}
|
||||
// 'incomplete' → readyAction stays undefined → run the full setup walk.
|
||||
}
|
||||
|
||||
const runOnly = readyAction;
|
||||
|
|
@ -872,7 +875,9 @@ async function runKtxSetupInner(args: KtxSetupArgs, io: KtxCliIo, deps: KtxSetup
|
|||
}
|
||||
if (step === 'context' && stepResult.status !== 'ready') {
|
||||
if (shouldRunAgents && args.skipAgents !== true) {
|
||||
return 0;
|
||||
// Context isn't built, so skip agent install — but still reach the
|
||||
// completion screen, which states readiness and points at `ktx ingest`.
|
||||
break setupLoop;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,7 @@ describe('KTX demo next steps', () => {
|
|||
agentIntegrationReady: true,
|
||||
}).join('\n');
|
||||
|
||||
expect(rendered).toContain('Build KTX context next.');
|
||||
expect(rendered).toContain('Run ingest to build database schema context before context-source ingest.');
|
||||
expect(rendered).toContain('Setup is complete. The only step left is to build context for your agents.');
|
||||
expect(rendered).toContain('ktx ingest');
|
||||
expect(rendered).not.toContain('resume');
|
||||
expect(rendered).not.toContain('scan');
|
||||
|
|
@ -87,6 +86,6 @@ describe('KTX demo next steps', () => {
|
|||
expect(rendered).toContain('ktx status --json');
|
||||
expect(rendered).not.toContain('ktx agent');
|
||||
expect(rendered).not.toContain('ktx serve --mcp stdio --user-id local');
|
||||
expect(rendered).not.toContain('Build KTX context next.');
|
||||
expect(rendered).not.toContain('Setup is complete.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { isKtxPreAgentSetupReady, isKtxSetupReady, runKtxSetupReadyChangeMenu } from '../src/setup-ready-menu.js';
|
||||
import {
|
||||
classifyKtxSetupCompletion,
|
||||
runKtxSetupReadyChangeMenu,
|
||||
runKtxSetupReadyMenu,
|
||||
} from '../src/setup-ready-menu.js';
|
||||
import type { KtxSetupStatus } from '../src/setup.js';
|
||||
|
||||
const readyStatus: KtxSetupStatus = {
|
||||
|
|
@ -13,32 +17,58 @@ const readyStatus: KtxSetupStatus = {
|
|||
agents: [{ target: 'codex', scope: 'project', ready: true }],
|
||||
};
|
||||
|
||||
describe('setup ready menu', () => {
|
||||
it('recognizes a ready setup only when required sections are ready', () => {
|
||||
expect(isKtxSetupReady(readyStatus)).toBe(true);
|
||||
expect(isKtxSetupReady({ ...readyStatus, embeddings: { ready: false } })).toBe(false);
|
||||
expect(isKtxSetupReady({ ...readyStatus, runtime: { required: true, ready: false, features: ['core'] } })).toBe(false);
|
||||
expect(isKtxSetupReady({ ...readyStatus, context: { ready: false, status: 'not_started' } })).toBe(false);
|
||||
expect(isKtxSetupReady({ ...readyStatus, agents: [] })).toBe(false);
|
||||
describe('classifyKtxSetupCompletion', () => {
|
||||
it('reports ready only when config, context, and agents are all ready', () => {
|
||||
expect(classifyKtxSetupCompletion(readyStatus)).toBe('ready');
|
||||
});
|
||||
|
||||
it('recognizes pre-agent readiness without requiring agents', () => {
|
||||
expect(isKtxPreAgentSetupReady(readyStatus)).toBe(true);
|
||||
expect(isKtxPreAgentSetupReady({ ...readyStatus, agents: [] })).toBe(true);
|
||||
expect(isKtxPreAgentSetupReady({ ...readyStatus, embeddings: { ready: false } })).toBe(false);
|
||||
expect(isKtxPreAgentSetupReady({ ...readyStatus, runtime: { required: true, ready: false, features: ['core'] } })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isKtxPreAgentSetupReady({ ...readyStatus, context: { ready: false, status: 'not_started' } })).toBe(false);
|
||||
it('reports needs-agents when config and context are ready but no agent is installed', () => {
|
||||
expect(classifyKtxSetupCompletion({ ...readyStatus, agents: [] })).toBe('needs-agents');
|
||||
});
|
||||
|
||||
it('maps ready-project menu choices to setup sections', async () => {
|
||||
const prompts = { select: vi.fn(async () => 'agents'), cancel: vi.fn() };
|
||||
it('reports needs-context when config is ready but context is not built', () => {
|
||||
expect(
|
||||
classifyKtxSetupCompletion({ ...readyStatus, context: { ready: false, status: 'not_started' } }),
|
||||
).toBe('needs-context');
|
||||
});
|
||||
|
||||
await expect(runKtxSetupReadyChangeMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'agents' });
|
||||
it('reports incomplete when a required config section is not ready', () => {
|
||||
expect(classifyKtxSetupCompletion({ ...readyStatus, embeddings: { ready: false } })).toBe('incomplete');
|
||||
expect(
|
||||
classifyKtxSetupCompletion({ ...readyStatus, runtime: { required: true, ready: false, features: ['core'] } }),
|
||||
).toBe('incomplete');
|
||||
});
|
||||
|
||||
it('reports incomplete when no context targets are configured', () => {
|
||||
expect(classifyKtxSetupCompletion({ ...readyStatus, databases: [], sources: [] })).toBe('incomplete');
|
||||
});
|
||||
});
|
||||
|
||||
describe('runKtxSetupReadyMenu', () => {
|
||||
it('exits when the user is done', async () => {
|
||||
const prompts = { select: vi.fn(async () => 'done'), cancel: vi.fn() };
|
||||
|
||||
await expect(runKtxSetupReadyMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'exit' });
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledTimes(1);
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'KTX is already set up for /tmp/revenue. What would you like to change?',
|
||||
message: 'Anything else?',
|
||||
options: [
|
||||
{ value: 'done', label: "Done — I'll start using ktx" },
|
||||
{ value: 'change', label: 'Change a setting' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('opens the section menu when the user chooses to change a setting', async () => {
|
||||
const select = vi.fn().mockResolvedValueOnce('change').mockResolvedValueOnce('models');
|
||||
const prompts = { select, cancel: vi.fn() };
|
||||
|
||||
await expect(runKtxSetupReadyMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'models' });
|
||||
|
||||
expect(select).toHaveBeenCalledTimes(2);
|
||||
expect(select).toHaveBeenLastCalledWith({
|
||||
message: 'What would you like to change?',
|
||||
options: [
|
||||
{ value: 'models', label: 'Models' },
|
||||
{ value: 'embeddings', label: 'Embeddings' },
|
||||
|
|
@ -51,3 +81,39 @@ describe('setup ready menu', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('runKtxSetupReadyChangeMenu', () => {
|
||||
it('maps ready-project menu choices to setup sections', async () => {
|
||||
const prompts = { select: vi.fn(async () => 'agents'), cancel: vi.fn() };
|
||||
|
||||
await expect(runKtxSetupReadyChangeMenu(readyStatus, { prompts })).resolves.toEqual({ action: 'agents' });
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith({
|
||||
message: 'What would you like to change?',
|
||||
options: [
|
||||
{ value: 'models', label: 'Models' },
|
||||
{ value: 'embeddings', label: 'Embeddings' },
|
||||
{ value: 'databases', label: 'Databases' },
|
||||
{ value: 'sources', label: 'Context sources' },
|
||||
{ value: 'context', label: 'Rebuild KTX context' },
|
||||
{ value: 'agents', label: 'Agent integration' },
|
||||
{ value: 'exit', label: 'Exit' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('includes the runtime option only when the runtime is required', async () => {
|
||||
const prompts = { select: vi.fn(async () => 'runtime'), cancel: vi.fn() };
|
||||
|
||||
await runKtxSetupReadyChangeMenu(
|
||||
{ ...readyStatus, runtime: { required: true, ready: true, features: ['core'] } },
|
||||
{ prompts },
|
||||
);
|
||||
|
||||
expect(prompts.select).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.arrayContaining([{ value: 'runtime', label: 'Runtime' }]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2205,8 +2205,11 @@ describe('setup status', () => {
|
|||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
' database_connection_ids: [warehouse]',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: anthropic',
|
||||
|
|
@ -2222,7 +2225,7 @@ describe('setup status', () => {
|
|||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, {
|
||||
completed_steps: ['project', 'llm', 'embeddings', 'sources', 'runtime', 'context', 'agents'],
|
||||
completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'runtime', 'context', 'agents'],
|
||||
});
|
||||
await writeFile(
|
||||
join(tempDir, '.ktx/agents/install-manifest.json'),
|
||||
|
|
@ -2275,7 +2278,12 @@ describe('setup status', () => {
|
|||
},
|
||||
io.io,
|
||||
{
|
||||
readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
|
||||
readyMenuDeps: {
|
||||
prompts: {
|
||||
select: vi.fn().mockResolvedValueOnce('change').mockResolvedValueOnce('agents'),
|
||||
cancel: vi.fn(),
|
||||
},
|
||||
},
|
||||
model: async (args) => {
|
||||
expect(args.skipLlm).toBe(true);
|
||||
return { status: 'skipped', projectDir: tempDir };
|
||||
|
|
@ -2325,8 +2333,11 @@ describe('setup status', () => {
|
|||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'setup:',
|
||||
' database_connection_ids: []',
|
||||
'connections: {}',
|
||||
' database_connection_ids: [warehouse]',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: anthropic',
|
||||
|
|
@ -2342,7 +2353,7 @@ describe('setup status', () => {
|
|||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, {
|
||||
completed_steps: ['project', 'llm', 'embeddings', 'sources', 'context'],
|
||||
completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'context'],
|
||||
});
|
||||
await writeKtxSetupContextState(tempDir, {
|
||||
runId: 'setup-context-local-ready',
|
||||
|
|
@ -2415,6 +2426,171 @@ describe('setup status', () => {
|
|||
expect(calls).toEqual(['agents']);
|
||||
});
|
||||
|
||||
it('routes a returning user to the context build when config is ready but context is not built', async () => {
|
||||
const calls: string[] = [];
|
||||
const io = makeIo();
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'setup:',
|
||||
' database_connection_ids: [warehouse]',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: anthropic',
|
||||
' models:',
|
||||
' default: claude-sonnet-4-6',
|
||||
'ingest:',
|
||||
' embeddings:',
|
||||
' backend: openai',
|
||||
' model: text-embedding-3-small',
|
||||
' dimensions: 1536',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, {
|
||||
completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'runtime'],
|
||||
});
|
||||
|
||||
const readyMenuSelect = vi.fn();
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: tempDir,
|
||||
mode: 'auto',
|
||||
agents: false,
|
||||
inputMode: 'auto',
|
||||
yes: false,
|
||||
cliVersion: '0.2.0',
|
||||
skipLlm: false,
|
||||
skipEmbeddings: false,
|
||||
skipDatabases: false,
|
||||
skipSources: false,
|
||||
skipAgents: false,
|
||||
databaseSchemas: [],
|
||||
},
|
||||
io.io,
|
||||
{
|
||||
readyMenuDeps: { prompts: { select: readyMenuSelect, cancel: vi.fn() } },
|
||||
model: async (args) => {
|
||||
expect(args.skipLlm).toBe(true);
|
||||
return { status: 'skipped', projectDir: tempDir };
|
||||
},
|
||||
embeddings: async (args) => {
|
||||
expect(args.skipEmbeddings).toBe(true);
|
||||
return { status: 'skipped', projectDir: tempDir };
|
||||
},
|
||||
databases: async (args) => {
|
||||
expect(args.skipDatabases).toBe(true);
|
||||
return { status: 'skipped', projectDir: tempDir };
|
||||
},
|
||||
sources: async (args) => {
|
||||
expect(args.skipSources).toBe(true);
|
||||
return { status: 'skipped', projectDir: tempDir };
|
||||
},
|
||||
runtime: async () => {
|
||||
calls.push('runtime');
|
||||
return runtimeReady(tempDir);
|
||||
},
|
||||
context: async (args) => {
|
||||
calls.push('context');
|
||||
expect(args.forcePrompt).toBe(true);
|
||||
return { status: 'skipped', projectDir: tempDir };
|
||||
},
|
||||
agents: async () => {
|
||||
calls.push('agents');
|
||||
return { status: 'ready', projectDir: tempDir, installs: [] };
|
||||
},
|
||||
},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
// Config is done, so the change-everything menu is not shown; setup routes straight
|
||||
// to the build prompt and never re-walks config or installs agents.
|
||||
expect(readyMenuSelect).not.toHaveBeenCalled();
|
||||
expect(calls).toContain('context');
|
||||
expect(calls).not.toContain('agents');
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Setup is complete. The only step left is to build context');
|
||||
expect(output).toContain('ktx ingest');
|
||||
});
|
||||
|
||||
it('reaches the completion screen instead of a bare shell when the context build is skipped', async () => {
|
||||
const calls: string[] = [];
|
||||
const io = makeIo();
|
||||
await writeFile(
|
||||
join(tempDir, 'ktx.yaml'),
|
||||
[
|
||||
'setup:',
|
||||
' database_connection_ids: [warehouse]',
|
||||
'connections:',
|
||||
' warehouse:',
|
||||
' driver: postgres',
|
||||
' url: env:DATABASE_URL',
|
||||
'llm:',
|
||||
' provider:',
|
||||
' backend: anthropic',
|
||||
' models:',
|
||||
' default: claude-sonnet-4-6',
|
||||
'ingest:',
|
||||
' embeddings:',
|
||||
' backend: openai',
|
||||
' model: text-embedding-3-small',
|
||||
' dimensions: 1536',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeKtxSetupState(tempDir, {
|
||||
completed_steps: ['project', 'llm', 'embeddings', 'databases', 'sources', 'runtime'],
|
||||
});
|
||||
|
||||
await expect(
|
||||
runKtxSetup(
|
||||
{
|
||||
command: 'run',
|
||||
projectDir: tempDir,
|
||||
mode: 'auto',
|
||||
agents: false,
|
||||
inputMode: 'disabled',
|
||||
yes: true,
|
||||
cliVersion: '0.2.0',
|
||||
skipLlm: true,
|
||||
skipEmbeddings: true,
|
||||
skipDatabases: true,
|
||||
skipSources: true,
|
||||
skipAgents: false,
|
||||
databaseSchemas: [],
|
||||
},
|
||||
io.io,
|
||||
{
|
||||
model: async () => ({ status: 'skipped', projectDir: tempDir }),
|
||||
embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
|
||||
databases: async () => ({ status: 'skipped', projectDir: tempDir }),
|
||||
sources: async () => ({ status: 'skipped', projectDir: tempDir }),
|
||||
runtime: async () => runtimeReady(tempDir),
|
||||
context: async () => ({ status: 'skipped', projectDir: tempDir }),
|
||||
agents: async () => {
|
||||
calls.push('agents');
|
||||
return { status: 'ready', projectDir: tempDir, installs: [] };
|
||||
},
|
||||
},
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
// A skipped build must not install agents nor drop to a bare shell; the end screen
|
||||
// states readiness and points at `ktx ingest`.
|
||||
expect(calls).not.toContain('agents');
|
||||
const output = io.stdout();
|
||||
expect(output).toContain('Setup is complete. The only step left is to build context');
|
||||
expect(output).toContain('ktx ingest');
|
||||
});
|
||||
|
||||
it('runs only project resolution and agent setup in --agents mode', async () => {
|
||||
const io = makeIo();
|
||||
const runtime = vi.fn(async () => runtimeReady(tempDir));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue