mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue