mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
merge: resolve conflict with main's box-drawing formatting
Keep the confirmProjectDir helper extraction from this branch while adopting the │ box-drawing prefixes added on main. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
ea10dd9f39
39 changed files with 1583 additions and 1313 deletions
|
|
@ -28,12 +28,12 @@ describe('prompt navigation helpers', () => {
|
|||
'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.',
|
||||
),
|
||||
).toBe(
|
||||
'Name this PostgreSQL connection\n\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n',
|
||||
'Name this PostgreSQL connection\n│\n│ KTX will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│',
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a blank separator before compact text input values', () => {
|
||||
expect(withTextInputNavigation('Project folder path')).toBe('Project folder path\nPress Escape to go back.\n');
|
||||
expect(withTextInputNavigation('Project folder path')).toBe('Project folder path\n│ Press Escape to go back.\n│');
|
||||
});
|
||||
|
||||
it('normalizes already hinted text input prompts without duplicating the hint', () => {
|
||||
|
|
@ -42,7 +42,19 @@ describe('prompt navigation helpers', () => {
|
|||
'Name this PostgreSQL connection\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.',
|
||||
),
|
||||
).toBe(
|
||||
'Name this PostgreSQL connection\n\nKTX will use this short name in commands and config. You can rename it now.\nPress Escape to go back.\n',
|
||||
'Name this PostgreSQL connection\n│\n│ KTX will use this short name in commands and config. You can rename it now.\n│ Press Escape to go back.\n│',
|
||||
);
|
||||
});
|
||||
|
||||
it('is idempotent when text input navigation is applied twice', () => {
|
||||
const once = withTextInputNavigation('Project folder path');
|
||||
expect(withTextInputNavigation(once)).toBe(once);
|
||||
});
|
||||
|
||||
it('is idempotent when text input navigation with body is applied twice', () => {
|
||||
const once = withTextInputNavigation(
|
||||
'Name this PostgreSQL connection\nKTX will use this short name in commands and config.',
|
||||
);
|
||||
expect(withTextInputNavigation(once)).toBe(once);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,26 @@ function removeTrailingBlankLines(message: string): string {
|
|||
return message.replace(/\n+$/, '');
|
||||
}
|
||||
|
||||
function prefixContinuationLines(message: string): string {
|
||||
const lines = message.split('\n');
|
||||
if (lines.length <= 1) return message;
|
||||
const [title, ...body] = lines;
|
||||
let trailingEmptyCount = 0;
|
||||
while (trailingEmptyCount < body.length && body[body.length - 1 - trailingEmptyCount] === '') {
|
||||
trailingEmptyCount++;
|
||||
}
|
||||
const contentBody = trailingEmptyCount > 0 ? body.slice(0, -trailingEmptyCount) : body;
|
||||
const trailingBody = trailingEmptyCount > 0 ? body.slice(-trailingEmptyCount) : [];
|
||||
return [
|
||||
title,
|
||||
...contentBody.map((line) => {
|
||||
const stripped = line.replace(/^│\s*/, '');
|
||||
return stripped === '' ? '│' : `│ ${stripped}`;
|
||||
}),
|
||||
...trailingBody,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function withTextInputBodySpacing(message: string): string {
|
||||
const normalized = removeTrailingBlankLines(message);
|
||||
if (!normalized.includes('\n')) {
|
||||
|
|
@ -39,7 +59,9 @@ export function withMultiselectNavigation(message: string): string {
|
|||
export function withTextInputNavigation(message: string): string {
|
||||
const messageWithoutHint = removeTrailingBlankLines(message)
|
||||
.split('\n')
|
||||
.filter((line) => line !== TEXT_INPUT_NAVIGATION_HINT)
|
||||
.filter((line) => !line.includes(TEXT_INPUT_NAVIGATION_HINT))
|
||||
.map((line) => line.replace(/^│\s*/, ''))
|
||||
.join('\n');
|
||||
return `${withTextInputBodySpacing(messageWithoutHint)}\n${TEXT_INPUT_NAVIGATION_HINT}\n`;
|
||||
const full = `${withTextInputBodySpacing(messageWithoutHint)}\n${TEXT_INPUT_NAVIGATION_HINT}`;
|
||||
return `${prefixContinuationLines(full)}\n│`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|||
import { dirname, join, relative, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { cancel, isCancel, multiselect, select } from '@clack/prompts';
|
||||
import { loadKtxProject, markKtxSetupStepComplete, serializeKtxProjectConfig } from '@ktx/context/project';
|
||||
import {
|
||||
loadKtxProject,
|
||||
markKtxSetupStateStepComplete,
|
||||
serializeKtxProjectConfig,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { withMenuOptionsSpacing, withMultiselectNavigation } from './prompt-navigation.js';
|
||||
import { withSetupInterruptConfirmation } from './setup-interrupt.js';
|
||||
|
|
@ -360,7 +365,8 @@ async function installTarget(input: {
|
|||
|
||||
async function markAgentsComplete(projectDir: string): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(markKtxSetupStepComplete(project.config, 'agents')), 'utf-8');
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(project.config)), 'utf-8');
|
||||
await markKtxSetupStateStepComplete(projectDir, 'agents');
|
||||
}
|
||||
|
||||
export async function runKtxSetupAgentsStep(
|
||||
|
|
@ -369,7 +375,7 @@ export async function runKtxSetupAgentsStep(
|
|||
deps: KtxSetupAgentsDeps = {},
|
||||
): Promise<KtxSetupAgentsResult> {
|
||||
if (args.skipAgents) {
|
||||
io.stdout.write('Agent integration skipped.\n');
|
||||
io.stdout.write('│ Agent integration skipped.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
if (!args.agents && args.inputMode === 'disabled') {
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import { cancel, isCancel, select } from '@clack/prompts';
|
|||
import {
|
||||
type KtxLocalProject,
|
||||
loadKtxProject,
|
||||
markKtxSetupStepComplete,
|
||||
ktxSetupCompletedSteps,
|
||||
markKtxSetupStateStepComplete,
|
||||
readKtxSetupState,
|
||||
serializeKtxProjectConfig,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { buildPublicIngestPlan } from './public-ingest.js';
|
||||
|
|
@ -467,11 +470,8 @@ async function defaultVerifyContextReady(projectDir: string): Promise<KtxSetupCo
|
|||
|
||||
async function markContextComplete(projectDir: string): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await writeFile(
|
||||
project.configPath,
|
||||
serializeKtxProjectConfig(markKtxSetupStepComplete(project.config, 'context')),
|
||||
'utf-8',
|
||||
);
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(project.config)), 'utf-8');
|
||||
await markKtxSetupStateStepComplete(projectDir, 'context');
|
||||
}
|
||||
|
||||
function writeBuildHeader(projectDir: string, runId: string, io: KtxCliIo): void {
|
||||
|
|
@ -714,7 +714,8 @@ export async function runKtxSetupContextStep(
|
|||
try {
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
const existingState = await readKtxSetupContextState(args.projectDir);
|
||||
if (project.config.setup?.completed_steps.includes('context') === true && existingState.status === 'completed') {
|
||||
const completedSteps = ktxSetupCompletedSteps(project.config, await readKtxSetupState(args.projectDir));
|
||||
if (completedSteps.includes('context') && existingState.status === 'completed') {
|
||||
return { status: 'ready', projectDir: args.projectDir, runId: existingState.runId ?? 'setup-context-completed' };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
|
||||
import { initKtxProject, parseKtxProjectConfig, readKtxSetupState } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
type KtxSetupDatabaseDriver,
|
||||
|
|
@ -58,10 +58,10 @@ function connectionNamePrompt(label: string): string {
|
|||
function textInputPrompt(message: string): string {
|
||||
const normalized = message.replace(/\n+$/, '');
|
||||
if (!normalized.includes('\n')) {
|
||||
return `${normalized}\nPress Escape to go back.\n`;
|
||||
return `${normalized}\n│ Press Escape to go back.\n│`;
|
||||
}
|
||||
const [title, ...bodyLines] = normalized.split('\n');
|
||||
return `${title}\n\n${bodyLines.join('\n')}\nPress Escape to go back.\n`;
|
||||
return `${title}\n│\n│ ${bodyLines.join('\n│ ')}\n│ Press Escape to go back.\n│`;
|
||||
}
|
||||
|
||||
const legacyHistoricSqlServiceAccountPatternsKey = ['serviceAccount', 'UserPatterns'].join('');
|
||||
|
|
@ -1091,8 +1091,9 @@ describe('setup databases step', () => {
|
|||
});
|
||||
expect(config.setup).toEqual({
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['databases'],
|
||||
completed_steps: [],
|
||||
});
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('databases');
|
||||
expect(io.stdout()).toContain('Primary source ready');
|
||||
expect(io.stdout()).not.toContain('DATABASE_URL=');
|
||||
});
|
||||
|
|
@ -1129,8 +1130,9 @@ describe('setup databases step', () => {
|
|||
});
|
||||
expect(config.setup).toEqual({
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['databases'],
|
||||
completed_steps: [],
|
||||
});
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('databases');
|
||||
});
|
||||
|
||||
it('selects multiple existing connections and validates each before recording setup ids', async () => {
|
||||
|
|
@ -1178,7 +1180,8 @@ describe('setup databases step', () => {
|
|||
expect(scanConnection).toHaveBeenCalledTimes(2);
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.setup?.database_connection_ids).toEqual(['warehouse', 'analytics']);
|
||||
expect(config.setup?.completed_steps).toContain('databases');
|
||||
expect(config.setup?.completed_steps).toEqual([]);
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('databases');
|
||||
});
|
||||
|
||||
it('keeps the connection config but does not mark databases complete when scanning fails', async () => {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ import type { HistoricSqlDialect } from '@ktx/context/ingest';
|
|||
import {
|
||||
type KtxProjectConnectionConfig,
|
||||
loadKtxProject,
|
||||
markKtxSetupStateStepComplete,
|
||||
serializeKtxProjectConfig,
|
||||
setKtxSetupDatabaseConnectionIds,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { runKtxConnection } from './connection.js';
|
||||
|
|
@ -923,7 +925,7 @@ async function writeConnectionConfig(input: {
|
|||
[input.connectionId]: input.connection,
|
||||
},
|
||||
};
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(config)), 'utf-8');
|
||||
|
||||
const historicSql =
|
||||
typeof input.connection.historicSql === 'object' &&
|
||||
|
|
@ -1076,25 +1078,28 @@ async function ensureHistoricSqlIngestDefaults(projectDir: string): Promise<void
|
|||
}
|
||||
await writeFile(
|
||||
project.configPath,
|
||||
serializeKtxProjectConfig({
|
||||
...project.config,
|
||||
ingest: {
|
||||
...project.config.ingest,
|
||||
adapters,
|
||||
workUnits: {
|
||||
...project.config.ingest.workUnits,
|
||||
maxConcurrency,
|
||||
serializeKtxProjectConfig(
|
||||
stripKtxSetupCompletedSteps({
|
||||
...project.config,
|
||||
ingest: {
|
||||
...project.config.ingest,
|
||||
adapters,
|
||||
workUnits: {
|
||||
...project.config.ingest.workUnits,
|
||||
maxConcurrency,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
async function markDatabasesComplete(projectDir: string, connectionIds: string[]): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const config = setKtxSetupDatabaseConnectionIds(project.config, unique(connectionIds), { complete: true });
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
const config = setKtxSetupDatabaseConnectionIds(project.config, unique(connectionIds));
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(config)), 'utf-8');
|
||||
await markKtxSetupStateStepComplete(projectDir, 'databases');
|
||||
}
|
||||
|
||||
async function maybeRunHistoricSqlSetupProbe(input: {
|
||||
|
|
@ -1110,7 +1115,7 @@ async function maybeRunHistoricSqlSetupProbe(input: {
|
|||
return;
|
||||
}
|
||||
|
||||
input.io.stdout.write('Historic SQL probe...\n');
|
||||
input.io.stdout.write('│ Historic SQL probe...\n');
|
||||
const probe = input.deps.historicSqlProbe ?? defaultHistoricSqlProbe;
|
||||
const result = await probe({
|
||||
projectDir: input.projectDir,
|
||||
|
|
@ -1118,10 +1123,10 @@ async function maybeRunHistoricSqlSetupProbe(input: {
|
|||
dialect: 'postgres',
|
||||
});
|
||||
for (const line of result.lines) {
|
||||
input.io.stdout.write(`${line}\n`);
|
||||
input.io.stdout.write(`│${line}\n`);
|
||||
}
|
||||
if (!result.ok) {
|
||||
input.io.stdout.write('Setup written; first ingest run will fail until fixed.\n');
|
||||
input.io.stdout.write('│ Setup written; first ingest run will fail until fixed.\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1256,7 +1261,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 primary source. Select a source or press Escape to go back.\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1320,7 +1325,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('│ Primary source setup skipped. KTX cannot work until you add a primary source.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
@ -1377,7 +1382,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 primary source.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
|
||||
import { initKtxProject, parseKtxProjectConfig, readKtxSetupState } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type KtxSetupEmbeddingsPromptAdapter, runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
|
||||
|
||||
|
|
@ -166,7 +166,8 @@ describe('setup embeddings step', () => {
|
|||
sentenceTransformers: { base_url: 'managed:local-embeddings', pathPrefix: '' },
|
||||
});
|
||||
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
||||
expect(config.setup?.completed_steps).toContain('embeddings');
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
||||
expect(io.stdout()).toContain(
|
||||
'Testing local sentence-transformers embeddings (all-MiniLM-L6-v2, 384 dimensions). First run may take up to 60 seconds.',
|
||||
);
|
||||
|
|
@ -198,7 +199,7 @@ describe('setup embeddings step', () => {
|
|||
|
||||
await vi.waitFor(() => {
|
||||
expect(io.stdout()).toContain(
|
||||
'\r- Testing local sentence-transformers embeddings (all-MiniLM-L6-v2, 384 dimensions). First run may take up to 60 seconds.',
|
||||
'\r│ - Testing local sentence-transformers embeddings (all-MiniLM-L6-v2, 384 dimensions). First run may take up to 60 seconds.',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -238,7 +239,8 @@ describe('setup embeddings step', () => {
|
|||
sentenceTransformers: { base_url: 'managed:local-embeddings', pathPrefix: '' },
|
||||
});
|
||||
expect(config.scan.enrichment.embeddings).toMatchObject(config.ingest.embeddings);
|
||||
expect(config.setup?.completed_steps).toContain('embeddings');
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('embeddings');
|
||||
});
|
||||
|
||||
it('fails non-interactive local setup when the managed local embeddings runtime is missing', async () => {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,12 @@ import { resolveKtxConfigReference } from '@ktx/context/core';
|
|||
import {
|
||||
type KtxProjectConfig,
|
||||
type KtxProjectEmbeddingConfig,
|
||||
ktxSetupCompletedSteps,
|
||||
loadKtxProject,
|
||||
markKtxSetupStepComplete,
|
||||
markKtxSetupStateStepComplete,
|
||||
readKtxSetupState,
|
||||
serializeKtxProjectConfig,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from '@ktx/context/project';
|
||||
import { type KtxEmbeddingConfig, type KtxEmbeddingHealthCheckResult, runKtxEmbeddingHealthCheck } from '@ktx/llm';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
|
|
@ -111,9 +114,9 @@ function createPromptAdapter(): KtxSetupEmbeddingsPromptAdapter {
|
|||
};
|
||||
}
|
||||
|
||||
function hasCompletedEmbeddings(config: KtxProjectConfig): boolean {
|
||||
async function hasCompletedEmbeddings(projectDir: string, config: KtxProjectConfig): Promise<boolean> {
|
||||
return (
|
||||
config.setup?.completed_steps.includes('embeddings') === true &&
|
||||
ktxSetupCompletedSteps(config, await readKtxSetupState(projectDir)).includes('embeddings') &&
|
||||
config.ingest.embeddings.backend !== 'none' &&
|
||||
config.ingest.embeddings.backend !== 'deterministic' &&
|
||||
typeof config.ingest.embeddings.model === 'string' &&
|
||||
|
|
@ -187,7 +190,7 @@ function embeddingBackendDisplayName(backend: KtxSetupEmbeddingBackend): string
|
|||
|
||||
async function persistEmbeddingConfig(projectDir: string, embeddings: KtxProjectEmbeddingConfig): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const config = markKtxSetupStepComplete(
|
||||
const config = stripKtxSetupCompletedSteps(
|
||||
{
|
||||
...project.config,
|
||||
ingest: {
|
||||
|
|
@ -202,9 +205,9 @@ async function persistEmbeddingConfig(projectDir: string, embeddings: KtxProject
|
|||
},
|
||||
},
|
||||
},
|
||||
'embeddings',
|
||||
);
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
await markKtxSetupStateStepComplete(projectDir, 'embeddings');
|
||||
}
|
||||
|
||||
async function chooseCredentialRef(
|
||||
|
|
@ -257,7 +260,7 @@ async function chooseCredentialRef(
|
|||
}
|
||||
if (choice === 'paste') {
|
||||
io.stdout.write(
|
||||
`${[
|
||||
`│ ${[
|
||||
`KTX will save the key in .ktx/secrets/${backend}-api-key with local file permissions,`,
|
||||
'then write a file: reference in ktx.yaml.',
|
||||
].join(' ')}\n`,
|
||||
|
|
@ -349,7 +352,7 @@ function healthCheckStartText(backend: KtxSetupEmbeddingBackend, model: string,
|
|||
|
||||
function startHealthCheckProgress(io: KtxCliIo, message: string): HealthCheckProgress {
|
||||
if (io.stdout.isTTY !== true) {
|
||||
io.stdout.write(`${message}\n`);
|
||||
io.stdout.write(`│ ${message}\n`);
|
||||
const noop = () => undefined;
|
||||
return {
|
||||
succeed: noop,
|
||||
|
|
@ -360,7 +363,7 @@ function startHealthCheckProgress(io: KtxCliIo, message: string): HealthCheckPro
|
|||
let frameIndex = 0;
|
||||
let stopped = false;
|
||||
const writeFrame = () => {
|
||||
io.stdout.write(`${CLEAR_CURRENT_LINE}${HEALTH_CHECK_SPINNER_FRAMES[frameIndex]} ${message}`);
|
||||
io.stdout.write(`${CLEAR_CURRENT_LINE}│ ${HEALTH_CHECK_SPINNER_FRAMES[frameIndex]} ${message}`);
|
||||
};
|
||||
writeFrame();
|
||||
const interval = setInterval(() => {
|
||||
|
|
@ -374,7 +377,7 @@ function startHealthCheckProgress(io: KtxCliIo, message: string): HealthCheckPro
|
|||
}
|
||||
stopped = true;
|
||||
clearInterval(interval);
|
||||
io.stdout.write(`${CLEAR_CURRENT_LINE}${finalMessage}\n`);
|
||||
io.stdout.write(`${CLEAR_CURRENT_LINE}│ ${finalMessage}\n`);
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -393,19 +396,19 @@ export async function runKtxSetupEmbeddingsStep(
|
|||
deps: KtxSetupEmbeddingsDeps = {},
|
||||
): Promise<KtxSetupEmbeddingsResult> {
|
||||
if (args.skipEmbeddings) {
|
||||
io.stdout.write('Embeddings setup skipped.\n');
|
||||
io.stdout.write('│ Embeddings setup skipped.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
const project = await loadKtxProject({ projectDir: args.projectDir });
|
||||
if (
|
||||
args.forcePrompt !== true &&
|
||||
hasCompletedEmbeddings(project.config) &&
|
||||
(await hasCompletedEmbeddings(args.projectDir, project.config)) &&
|
||||
!args.embeddingBackend &&
|
||||
!args.embeddingApiKeyEnv &&
|
||||
!args.embeddingApiKeyFile
|
||||
) {
|
||||
io.stdout.write(`Embeddings ready: yes (${project.config.ingest.embeddings.model})\n`);
|
||||
io.stdout.write(`│ Embeddings ready: yes (${project.config.ingest.embeddings.model})\n`);
|
||||
return { status: 'ready', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
@ -492,7 +495,7 @@ export async function runKtxSetupEmbeddingsStep(
|
|||
credentialRef,
|
||||
}),
|
||||
);
|
||||
io.stdout.write(`Embeddings ready: yes (${model}, ${dimensions} dimensions)\n`);
|
||||
io.stdout.write(`│ Embeddings ready: yes (${model}, ${dimensions} dimensions)\n`);
|
||||
return { status: 'ready', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
|
||||
import { initKtxProject, parseKtxProjectConfig, readKtxSetupState } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
BUNDLED_ANTHROPIC_MODELS,
|
||||
|
|
@ -160,7 +160,8 @@ describe('setup Anthropic model step', () => {
|
|||
promptCaching: { enabled: true },
|
||||
});
|
||||
expect(config.scan.enrichment.mode).toBe('llm');
|
||||
expect(config.setup?.completed_steps).toContain('llm');
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('llm');
|
||||
expect(io.stdout()).toContain('LLM ready: yes');
|
||||
expect(io.stdout()).not.toContain('sk-ant-test');
|
||||
});
|
||||
|
|
@ -198,7 +199,8 @@ describe('setup Anthropic model step', () => {
|
|||
},
|
||||
models: { default: 'claude-sonnet-4-6' },
|
||||
});
|
||||
expect(config.setup?.completed_steps).toContain('llm');
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('llm');
|
||||
expect(io.stdout()).not.toContain('sk-ant-file');
|
||||
});
|
||||
|
||||
|
|
@ -310,7 +312,7 @@ describe('setup Anthropic model step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(prompts.select).not.toHaveBeenCalledWith(expect.objectContaining({ message: 'Paste Anthropic API key now?' }));
|
||||
expect(prompts.password).toHaveBeenCalledWith({
|
||||
message: 'Anthropic API key\nPress Escape to go back.\n',
|
||||
message: 'Anthropic API key\n│ Press Escape to go back.\n│',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -462,7 +464,7 @@ describe('setup Anthropic model step', () => {
|
|||
);
|
||||
expect(prompts.text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Anthropic model ID\nPress Escape to go back.\n',
|
||||
message: 'Anthropic model ID\n│ Press Escape to go back.\n│',
|
||||
placeholder: 'claude-sonnet-4-6',
|
||||
}),
|
||||
);
|
||||
|
|
@ -551,7 +553,8 @@ describe('setup Anthropic model step', () => {
|
|||
expect(io.stderr()).toContain('Choose a different credential source or model, or Back.');
|
||||
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.llm.models.default).toBe('claude-sonnet-4-6');
|
||||
expect(config.setup?.completed_steps).toContain('llm');
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect((await readKtxSetupState(tempDir)).completed_steps).toContain('llm');
|
||||
expect(io.stderr()).not.toContain('sk-ant-test');
|
||||
});
|
||||
|
||||
|
|
@ -626,7 +629,7 @@ describe('setup Anthropic model step', () => {
|
|||
|
||||
expect(result.status).toBe('ready');
|
||||
expect(prompts.password).toHaveBeenCalledWith({
|
||||
message: 'Anthropic API key\nPress Escape to go back.\n',
|
||||
message: 'Anthropic API key\n│ Press Escape to go back.\n│',
|
||||
});
|
||||
await expect(readFile(join(tempDir, '.ktx/secrets/anthropic-api-key'), 'utf-8')).rejects.toMatchObject({
|
||||
code: 'ENOENT',
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import {
|
|||
type KtxProjectConfig,
|
||||
type KtxProjectLlmConfig,
|
||||
loadKtxProject,
|
||||
markKtxSetupStepComplete,
|
||||
markKtxSetupStateStepComplete,
|
||||
serializeKtxProjectConfig,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from '@ktx/context/project';
|
||||
import { type KtxLlmConfig, type KtxLlmHealthCheckResult, runKtxLlmHealthCheck } from '@ktx/llm';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
|
|
@ -254,7 +255,7 @@ async function chooseCredentialRef(
|
|||
const prompts = deps.prompts ?? createPromptAdapter();
|
||||
if (args.showPromptInstructions !== false) {
|
||||
io.stdout.write(
|
||||
'Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n',
|
||||
'│ Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n',
|
||||
);
|
||||
}
|
||||
while (true) {
|
||||
|
|
@ -271,7 +272,7 @@ async function chooseCredentialRef(
|
|||
}
|
||||
if (choice === 'paste') {
|
||||
io.stdout.write(
|
||||
'KTX will save the key in .ktx/secrets/anthropic-api-key with local file permissions, then write a file: reference in ktx.yaml.\n',
|
||||
'│ KTX will save the key in .ktx/secrets/anthropic-api-key with local file permissions, then write a file: reference in ktx.yaml.\n',
|
||||
);
|
||||
const value = await prompts.password({ message: withTextInputNavigation('Anthropic API key') });
|
||||
if (value === undefined) {
|
||||
|
|
@ -361,7 +362,7 @@ async function chooseModel(
|
|||
|
||||
async function persistLlmConfig(projectDir: string, credentialRef: string, model: string): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
const config = markKtxSetupStepComplete(
|
||||
const config = stripKtxSetupCompletedSteps(
|
||||
{
|
||||
...project.config,
|
||||
llm: buildProjectLlmConfig(project.config.llm, credentialRef, model),
|
||||
|
|
@ -373,9 +374,9 @@ async function persistLlmConfig(projectDir: string, credentialRef: string, model
|
|||
},
|
||||
},
|
||||
},
|
||||
'llm',
|
||||
);
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
await markKtxSetupStateStepComplete(projectDir, 'llm');
|
||||
}
|
||||
|
||||
function buildInteractiveRetryArgs(args: KtxSetupModelArgs): KtxSetupModelArgs {
|
||||
|
|
@ -393,7 +394,7 @@ export async function runKtxSetupAnthropicModelStep(
|
|||
deps: KtxSetupModelDeps = {},
|
||||
): Promise<KtxSetupModelResult> {
|
||||
if (args.skipLlm) {
|
||||
io.stdout.write('LLM setup skipped.\n');
|
||||
io.stdout.write('│ LLM setup skipped.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
@ -405,7 +406,7 @@ export async function runKtxSetupAnthropicModelStep(
|
|||
!args.anthropicApiKeyFile &&
|
||||
!args.anthropicModel
|
||||
) {
|
||||
io.stdout.write(`LLM ready: yes (${project.config.llm.models.default})\n`);
|
||||
io.stdout.write(`│ LLM ready: yes (${project.config.llm.models.default})\n`);
|
||||
return { status: 'ready', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
@ -438,7 +439,7 @@ export async function runKtxSetupAnthropicModelStep(
|
|||
const health = await healthCheck(buildHealthConfig(credential.value, model.model));
|
||||
if (health.ok) {
|
||||
await persistLlmConfig(args.projectDir, credential.ref, model.model);
|
||||
io.stdout.write(`LLM ready: yes (${model.model})\n`);
|
||||
io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
|
||||
return { status: 'ready', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { initKtxProject, parseKtxProjectConfig } from '@ktx/context/project';
|
||||
import { initKtxProject, parseKtxProjectConfig, readKtxSetupState } from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { type KtxSetupProjectPromptAdapter, runKtxSetupProjectStep } from './setup-project.js';
|
||||
|
||||
|
|
@ -60,7 +60,8 @@ describe('setup project step', () => {
|
|||
expect(result.status).toBe('ready');
|
||||
expect(result.projectDir).toBe(projectDir);
|
||||
const config = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.setup?.completed_steps).toEqual(['project']);
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect(await readKtxSetupState(projectDir)).toEqual({ completed_steps: ['project'] });
|
||||
await expect(stat(join(projectDir, '.git'))).resolves.toBeDefined();
|
||||
await expect(readFile(join(projectDir, '.ktx/.gitignore'), 'utf-8')).resolves.toContain('secrets/');
|
||||
expect(testIo.stdout()).toContain(`Project: ${projectDir}`);
|
||||
|
|
@ -93,8 +94,9 @@ describe('setup project step', () => {
|
|||
const config = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.setup).toEqual({
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['llm', 'project'],
|
||||
completed_steps: [],
|
||||
});
|
||||
expect(await readKtxSetupState(projectDir)).toEqual({ completed_steps: ['llm', 'project'] });
|
||||
});
|
||||
|
||||
it('creates a missing auto-mode project only when --yes is present in no-input mode', async () => {
|
||||
|
|
@ -151,7 +153,8 @@ describe('setup project step', () => {
|
|||
);
|
||||
expect(prompts.text).not.toHaveBeenCalled();
|
||||
const config = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
||||
expect(config.setup?.completed_steps).toEqual(['project']);
|
||||
expect(config.setup?.completed_steps).toEqual(undefined);
|
||||
expect(await readKtxSetupState(projectDir)).toEqual({ completed_steps: ['project'] });
|
||||
});
|
||||
|
||||
it('offers an absolute default destination for a new project folder', async () => {
|
||||
|
|
@ -183,7 +186,7 @@ describe('setup project step', () => {
|
|||
);
|
||||
expect(prompts.text).not.toHaveBeenCalled();
|
||||
expect(result.status === 'ready' ? result.project.config.project : '').toBe('ktx-project');
|
||||
expect(testIo.stdout()).toContain(`KTX will create:\n ${projectDir}`);
|
||||
expect(testIo.stdout()).toContain(`│ KTX will create:\n│ ${projectDir}`);
|
||||
await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
|
|
@ -202,7 +205,7 @@ describe('setup project step', () => {
|
|||
expect(result.projectDir).toBe(projectDir);
|
||||
expect(prompts.text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Project folder path\nPress Escape to go back.\n',
|
||||
message: 'Project folder path\n│ Press Escape to go back.\n│',
|
||||
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import { basename, join, resolve } from 'node:path';
|
|||
import { cancel, isCancel, select, text } from '@clack/prompts';
|
||||
import {
|
||||
initKtxProject,
|
||||
ktxSetupCompletedSteps,
|
||||
type KtxLocalProject,
|
||||
loadKtxProject,
|
||||
markKtxSetupStepComplete,
|
||||
markKtxSetupStateStepComplete,
|
||||
mergeKtxSetupGitignoreEntries,
|
||||
readKtxSetupState,
|
||||
serializeKtxProjectConfig,
|
||||
stripKtxSetupCompletedSteps,
|
||||
writeKtxSetupState,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { withMenuOptionsSpacing, withTextInputNavigation } from './prompt-navigation.js';
|
||||
|
|
@ -143,7 +147,7 @@ async function confirmProjectDir(
|
|||
return { status: 'confirmed', confirmedCreation: true };
|
||||
}
|
||||
|
||||
io.stdout.write(`KTX will create:\n ${selectedDir}\n`);
|
||||
io.stdout.write(`│ KTX will create:\n│ ${selectedDir}\n`);
|
||||
const action = await prompts.select({
|
||||
message: `Create KTX project at ${selectedDir}?`,
|
||||
options: [
|
||||
|
|
@ -166,8 +170,11 @@ async function normalizeSetupGitignore(projectDir: string): Promise<void> {
|
|||
}
|
||||
|
||||
async function persistProjectStep(project: KtxLocalProject): Promise<KtxLocalProject> {
|
||||
const config = markKtxSetupStepComplete(project.config, 'project');
|
||||
const completedSteps = ktxSetupCompletedSteps(project.config, await readKtxSetupState(project.projectDir));
|
||||
const config = stripKtxSetupCompletedSteps(project.config);
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
await writeKtxSetupState(project.projectDir, { completed_steps: completedSteps });
|
||||
await markKtxSetupStateStepComplete(project.projectDir, 'project');
|
||||
await normalizeSetupGitignore(project.projectDir);
|
||||
return await loadKtxProject({ projectDir: project.projectDir });
|
||||
}
|
||||
|
|
@ -185,7 +192,7 @@ async function loadExistingProject(projectDir: string, deps: KtxSetupProjectDeps
|
|||
}
|
||||
|
||||
function printProjectSummary(io: KtxCliIo, projectDir: string): void {
|
||||
io.stdout.write(`Project: ${projectDir}\n`);
|
||||
io.stdout.write(`│ Project: ${projectDir}\n`);
|
||||
}
|
||||
|
||||
async function promptForNewProjectDir(
|
||||
|
|
@ -197,8 +204,8 @@ async function promptForNewProjectDir(
|
|||
const defaultProjectDir = join(projectDir, DEFAULT_NEW_PROJECT_FOLDER_NAME);
|
||||
|
||||
while (true) {
|
||||
io.stdout.write(`Relative paths are resolved from:\n ${projectDir}\n`);
|
||||
io.stdout.write(`Home paths are resolved from:\n ${homeDir}\n`);
|
||||
io.stdout.write(`│ Relative paths are resolved from:\n│ ${projectDir}\n`);
|
||||
io.stdout.write(`│ Home paths are resolved from:\n│ ${homeDir}\n`);
|
||||
const destinationChoice = await prompts.select({
|
||||
message: 'Where should KTX create the project?',
|
||||
options: [
|
||||
|
|
@ -324,7 +331,7 @@ export async function runKtxSetupProjectStep(
|
|||
const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
|
||||
const defaultProjectDir = join(projectDir, DEFAULT_NEW_PROJECT_FOLDER_NAME);
|
||||
io.stdout.write(
|
||||
'Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n',
|
||||
'│ Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n',
|
||||
);
|
||||
while (true) {
|
||||
const choice = await prompts.select({
|
||||
|
|
@ -369,8 +376,8 @@ export async function runKtxSetupProjectStep(
|
|||
}
|
||||
|
||||
if (choice === 'new-custom') {
|
||||
io.stdout.write(`Relative paths are resolved from:\n ${projectDir}\n`);
|
||||
io.stdout.write(`Home paths are resolved from:\n ${homeDir}\n`);
|
||||
io.stdout.write(`│ Relative paths are resolved from:\n│ ${projectDir}\n`);
|
||||
io.stdout.write(`│ Home paths are resolved from:\n│ ${homeDir}\n`);
|
||||
const rawPath = await prompts.text({
|
||||
message: withTextInputNavigation('Project folder path'),
|
||||
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
initKtxProject,
|
||||
type KtxProjectConnectionConfig,
|
||||
parseKtxProjectConfig,
|
||||
readKtxSetupState,
|
||||
serializeKtxProjectConfig,
|
||||
} from '@ktx/context/project';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
|
@ -65,10 +66,10 @@ function connectionNamePrompt(label: string): string {
|
|||
function textInputPrompt(message: string): string {
|
||||
const normalized = message.replace(/\n+$/, '');
|
||||
if (!normalized.includes('\n')) {
|
||||
return `${normalized}\nPress Escape to go back.\n`;
|
||||
return `${normalized}\n│ Press Escape to go back.\n│`;
|
||||
}
|
||||
const [title, ...bodyLines] = normalized.split('\n');
|
||||
return `${title}\n\n${bodyLines.join('\n')}\nPress Escape to go back.\n`;
|
||||
return `${title}\n│\n│ ${bodyLines.join('\n│ ')}\n│ Press Escape to go back.\n│`;
|
||||
}
|
||||
|
||||
describe('setup sources step', () => {
|
||||
|
|
@ -136,7 +137,8 @@ describe('setup sources step', () => {
|
|||
projectDir,
|
||||
});
|
||||
|
||||
expect((await readConfig()).setup?.completed_steps).toContain('sources');
|
||||
expect((await readConfig()).setup?.completed_steps).toEqual(undefined);
|
||||
expect((await readKtxSetupState(projectDir)).completed_steps).toContain('sources');
|
||||
expect(io.stdout()).toContain('Context source setup skipped.');
|
||||
});
|
||||
|
||||
|
|
@ -169,7 +171,8 @@ describe('setup sources step', () => {
|
|||
source_dir: '/repo/dbt',
|
||||
project_name: 'analytics',
|
||||
});
|
||||
expect(config.setup?.completed_steps).toContain('sources');
|
||||
expect(config.setup?.completed_steps).toEqual([]);
|
||||
expect((await readKtxSetupState(projectDir)).completed_steps).toContain('sources');
|
||||
expect(runInitialIngest).toHaveBeenCalledWith(projectDir, 'analytics_dbt', io.io, { inputMode: 'disabled' });
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ import {
|
|||
type KtxProjectConfig,
|
||||
type KtxProjectConnectionConfig,
|
||||
loadKtxProject,
|
||||
markKtxSetupStepComplete,
|
||||
markKtxSetupStateStepComplete,
|
||||
serializeKtxProjectConfig,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { runKtxConnectionMapping } from './commands/connection-mapping.js';
|
||||
|
|
@ -333,7 +334,7 @@ function fileRepoUrl(sourceDir: string): string {
|
|||
|
||||
async function writeProjectConfig(projectDir: string, config: KtxProjectConfig): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(config)), 'utf-8');
|
||||
}
|
||||
|
||||
async function writeSourceConnection(
|
||||
|
|
@ -360,7 +361,7 @@ async function writeSourceConnection(
|
|||
: [...project.config.ingest.adapters, adapter],
|
||||
},
|
||||
};
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(config), 'utf-8');
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(config)), 'utf-8');
|
||||
return async () => {
|
||||
const latest = await loadKtxProject({ projectDir });
|
||||
const connections = { ...latest.config.connections };
|
||||
|
|
@ -399,11 +400,8 @@ async function ensureSourceAdapterEnabled(projectDir: string, source: KtxSetupSo
|
|||
|
||||
async function markSourcesComplete(projectDir: string): Promise<void> {
|
||||
const project = await loadKtxProject({ projectDir });
|
||||
await writeFile(
|
||||
project.configPath,
|
||||
serializeKtxProjectConfig(markKtxSetupStepComplete(project.config, 'sources')),
|
||||
'utf-8',
|
||||
);
|
||||
await writeFile(project.configPath, serializeKtxProjectConfig(stripKtxSetupCompletedSteps(project.config)), 'utf-8');
|
||||
await markKtxSetupStateStepComplete(projectDir, 'sources');
|
||||
}
|
||||
|
||||
function hasPrimarySource(config: KtxProjectConfig): boolean {
|
||||
|
|
@ -701,7 +699,7 @@ async function runInitialSourceIngestWithRecovery(input: {
|
|||
deps: KtxSetupSourcesDeps;
|
||||
}): Promise<'ready' | 'continue' | 'back' | 'failed'> {
|
||||
while (true) {
|
||||
input.io.stdout.write(`Building context from ${input.connectionId}. Large sources can take a while.\n`);
|
||||
input.io.stdout.write(`│ Building context from ${input.connectionId}. Large sources can take a while.\n`);
|
||||
const ingestCode = await (input.deps.runInitialIngest ?? defaultRunInitialIngest)(
|
||||
input.args.projectDir,
|
||||
input.connectionId,
|
||||
|
|
@ -729,8 +727,8 @@ async function runInitialSourceIngestWithRecovery(input: {
|
|||
continue;
|
||||
}
|
||||
if (action === 'continue') {
|
||||
input.io.stdout.write(`Context source saved without a completed context build for ${input.connectionId}.\n`);
|
||||
input.io.stdout.write(`Run later: ktx ingest ${input.connectionId}\n`);
|
||||
input.io.stdout.write(`│ Context source saved without a completed context build for ${input.connectionId}.\n`);
|
||||
input.io.stdout.write(`│ Run later: ktx ingest ${input.connectionId}\n`);
|
||||
return 'continue';
|
||||
}
|
||||
return 'back';
|
||||
|
|
@ -1357,7 +1355,7 @@ export async function runKtxSetupSourcesStep(
|
|||
try {
|
||||
if (args.skipSources) {
|
||||
await markSourcesComplete(args.projectDir);
|
||||
io.stdout.write('Context source setup skipped.\n');
|
||||
io.stdout.write('│ Context source setup skipped.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
@ -1370,7 +1368,7 @@ export async function runKtxSetupSourcesStep(
|
|||
return { status: 'failed', projectDir: args.projectDir };
|
||||
}
|
||||
if (args.inputMode !== 'disabled') {
|
||||
io.stdout.write(`${message}\n`);
|
||||
io.stdout.write(`│ ${message}\n`);
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
}
|
||||
|
|
@ -1394,7 +1392,7 @@ export async function runKtxSetupSourcesStep(
|
|||
return { status: 'missing-input', projectDir: args.projectDir };
|
||||
}
|
||||
await markSourcesComplete(args.projectDir);
|
||||
io.stdout.write('No context sources selected.\n');
|
||||
io.stdout.write('│ No context sources selected.\n');
|
||||
return { status: 'skipped', projectDir: args.projectDir };
|
||||
}
|
||||
|
||||
|
|
@ -1467,7 +1465,7 @@ export async function runKtxSetupSourcesStep(
|
|||
break;
|
||||
}
|
||||
} else {
|
||||
io.stdout.write(`Context source ${connectionId} saved. It will be built during the context build step.\n`);
|
||||
io.stdout.write(`│ Context source ${connectionId} saved. It will be built during the context build step.\n`);
|
||||
}
|
||||
readyConnectionIds.push(connectionId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -715,7 +715,7 @@ describe('setup status', () => {
|
|||
|
||||
expect(projectPrompts.text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Project folder path\nPress Escape to go back.\n',
|
||||
message: 'Project folder path\n│ Press Escape to go back.\n│',
|
||||
placeholder: './analytics-ktx, ~/analytics-ktx, or /Users/you/projects/analytics-ktx',
|
||||
}),
|
||||
);
|
||||
|
|
@ -839,7 +839,10 @@ describe('setup status', () => {
|
|||
).resolves.toBe(0);
|
||||
|
||||
await expect(stat(join(tempDir, 'ktx.yaml'))).resolves.toBeDefined();
|
||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).toContain('completed_steps:');
|
||||
expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
|
||||
await expect(readFile(join(tempDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toBe(
|
||||
`${JSON.stringify({ completed_steps: ['project', 'sources'] }, null, 2)}\n`,
|
||||
);
|
||||
expect(testIo.stdout()).toContain('KTX setup');
|
||||
expect(testIo.stdout()).toContain(`Project: ${tempDir}`);
|
||||
expect(testIo.stdout()).toContain('Project ready: yes');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import { existsSync } from 'node:fs';
|
|||
import { join, resolve } from 'node:path';
|
||||
import { cancel, isCancel, select } from '@clack/prompts';
|
||||
import { getLatestLocalIngestStatus, savedMemoryCountsForReport } from '@ktx/context/ingest';
|
||||
import { ktxLocalStateDbPath, loadKtxProject, type KtxLocalProject } from '@ktx/context/project';
|
||||
import {
|
||||
ktxLocalStateDbPath,
|
||||
ktxSetupCompletedSteps,
|
||||
loadKtxProject,
|
||||
readKtxSetupState,
|
||||
type KtxLocalProject,
|
||||
} from '@ktx/context/project';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { formatSetupNextStepLines } from './next-steps.js';
|
||||
import { isKtxSetupExitError, withSetupInterruptConfirmation } from './setup-interrupt.js';
|
||||
|
|
@ -291,7 +297,7 @@ export async function readKtxSetupStatus(projectDir: string): Promise<KtxSetupSt
|
|||
};
|
||||
embeddings.ready = embeddingsReady(embeddings);
|
||||
|
||||
const completedSteps = project.config.setup?.completed_steps ?? [];
|
||||
const completedSteps = ktxSetupCompletedSteps(project.config, await readKtxSetupState(resolvedProjectDir));
|
||||
const contextState = await readKtxSetupContextState(resolvedProjectDir);
|
||||
const setupContextStatus = setupContextStatusFromState(contextState, {
|
||||
completedStep: completedSteps.includes('context'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue