mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
feat(context): add local .ktx/setup/state.json for setup completion tracking
Move setup step completion state out of ktx.yaml into a gitignored local state file so it is not committed or shared across machines. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
60457e9407
commit
f70271152b
4 changed files with 121 additions and 50 deletions
|
|
@ -75,7 +75,7 @@ export interface KtxProjectConnectionConfig {
|
|||
|
||||
export interface KtxProjectSetupConfig {
|
||||
database_connection_ids: string[];
|
||||
completed_steps: string[];
|
||||
completed_steps?: string[];
|
||||
}
|
||||
|
||||
export interface KtxProjectConfig {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,12 @@ export { initKtxProject, loadKtxProject } from './project.js';
|
|||
export type { KtxSetupStep } from './setup-config.js';
|
||||
export {
|
||||
KTX_SETUP_STEPS,
|
||||
markKtxSetupStepComplete,
|
||||
ktxSetupCompletedSteps,
|
||||
ktxSetupStatePath,
|
||||
markKtxSetupStateStepComplete,
|
||||
mergeKtxSetupGitignoreEntries,
|
||||
readKtxSetupState,
|
||||
setKtxSetupDatabaseConnectionIds,
|
||||
stripKtxSetupCompletedSteps,
|
||||
writeKtxSetupState,
|
||||
} from './setup-config.js';
|
||||
|
|
|
|||
|
|
@ -1,43 +1,40 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { buildDefaultKtxProjectConfig } from './config.js';
|
||||
import {
|
||||
markKtxSetupStepComplete,
|
||||
ktxSetupCompletedSteps,
|
||||
markKtxSetupStateStepComplete,
|
||||
mergeKtxSetupGitignoreEntries,
|
||||
readKtxSetupState,
|
||||
setKtxSetupDatabaseConnectionIds,
|
||||
stripKtxSetupCompletedSteps,
|
||||
} from './setup-config.js';
|
||||
|
||||
describe('KTX setup config helpers', () => {
|
||||
it('marks setup steps complete without duplicating existing state', () => {
|
||||
const config = buildDefaultKtxProjectConfig('warehouse');
|
||||
let tempDir: string;
|
||||
|
||||
const withProject = markKtxSetupStepComplete(config, 'project');
|
||||
const withProjectAgain = markKtxSetupStepComplete(withProject, 'project');
|
||||
const withLlm = markKtxSetupStepComplete(withProjectAgain, 'llm');
|
||||
const withContext = markKtxSetupStepComplete(withLlm, 'context');
|
||||
|
||||
expect(withProject.setup).toEqual({
|
||||
database_connection_ids: [],
|
||||
completed_steps: ['project'],
|
||||
});
|
||||
expect(withProjectAgain.setup?.completed_steps).toEqual(['project']);
|
||||
expect(withLlm.setup?.completed_steps).toEqual(['project', 'llm']);
|
||||
expect(withContext.setup?.completed_steps).toEqual(['project', 'llm', 'context']);
|
||||
expect(config.setup).toBeUndefined();
|
||||
beforeEach(async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'ktx-setup-state-'));
|
||||
});
|
||||
|
||||
it('preserves database connection ids while marking a step complete', () => {
|
||||
const config = {
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
setup: {
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['databases'],
|
||||
},
|
||||
};
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
expect(markKtxSetupStepComplete(config, 'project').setup).toEqual({
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['databases', 'project'],
|
||||
it('marks setup steps complete in local state without duplicating existing state', async () => {
|
||||
await markKtxSetupStateStepComplete(tempDir, 'project');
|
||||
await markKtxSetupStateStepComplete(tempDir, 'project');
|
||||
await markKtxSetupStateStepComplete(tempDir, 'llm');
|
||||
await markKtxSetupStateStepComplete(tempDir, 'context');
|
||||
|
||||
expect(await readKtxSetupState(tempDir)).toEqual({
|
||||
completed_steps: ['project', 'llm', 'context'],
|
||||
});
|
||||
await expect(readFile(join(tempDir, '.ktx', 'setup', 'state.json'), 'utf-8')).resolves.toBe(
|
||||
`${JSON.stringify({ completed_steps: ['project', 'llm', 'context'] }, null, 2)}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it('sets setup database connection ids without duplicates', () => {
|
||||
|
|
@ -47,22 +44,38 @@ describe('KTX setup config helpers', () => {
|
|||
|
||||
expect(withDatabases.setup).toEqual({
|
||||
database_connection_ids: ['warehouse', 'analytics'],
|
||||
completed_steps: [],
|
||||
});
|
||||
expect(config.setup).toBeUndefined();
|
||||
});
|
||||
|
||||
it('marks databases complete only when requested', () => {
|
||||
const config = markKtxSetupStepComplete(buildDefaultKtxProjectConfig('warehouse'), 'project');
|
||||
it('strips setup completed steps while preserving database connection ids', () => {
|
||||
const config = {
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
setup: {
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['project', 'databases'],
|
||||
},
|
||||
};
|
||||
|
||||
const withDatabases = setKtxSetupDatabaseConnectionIds(config, ['warehouse'], { complete: true });
|
||||
const withDatabasesAgain = setKtxSetupDatabaseConnectionIds(withDatabases, ['warehouse'], { complete: true });
|
||||
|
||||
expect(withDatabases.setup).toEqual({
|
||||
expect(stripKtxSetupCompletedSteps(config).setup).toEqual({
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['project', 'databases'],
|
||||
});
|
||||
expect(withDatabasesAgain.setup).toEqual(withDatabases.setup);
|
||||
});
|
||||
|
||||
it('combines legacy config setup steps with local state for reads', () => {
|
||||
const config = {
|
||||
...buildDefaultKtxProjectConfig('warehouse'),
|
||||
setup: {
|
||||
database_connection_ids: ['warehouse'],
|
||||
completed_steps: ['project', 'databases'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(ktxSetupCompletedSteps(config, { completed_steps: ['databases', 'sources'] })).toEqual([
|
||||
'project',
|
||||
'databases',
|
||||
'sources',
|
||||
]);
|
||||
});
|
||||
|
||||
it('merges setup-local gitignore entries without removing existing lines', () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import type { KtxProjectConfig } from './config.js';
|
||||
|
||||
export const KTX_SETUP_STEPS = ['project', 'llm', 'embeddings', 'databases', 'sources', 'context', 'agents'] as const;
|
||||
|
||||
export type KtxSetupStep = (typeof KTX_SETUP_STEPS)[number];
|
||||
|
||||
export interface KtxSetupState {
|
||||
completed_steps: KtxSetupStep[];
|
||||
}
|
||||
|
||||
const SETUP_GITIGNORE_ENTRIES = [
|
||||
'cache/',
|
||||
'db.sqlite',
|
||||
|
|
@ -14,14 +20,67 @@ const SETUP_GITIGNORE_ENTRIES = [
|
|||
'agents/',
|
||||
] as const;
|
||||
|
||||
export function markKtxSetupStepComplete(config: KtxProjectConfig, step: KtxSetupStep): KtxProjectConfig {
|
||||
const databaseConnectionIds = config.setup?.database_connection_ids ?? [];
|
||||
const completedSteps = config.setup?.completed_steps ?? [];
|
||||
function isKtxSetupStep(value: unknown): value is KtxSetupStep {
|
||||
return typeof value === 'string' && (KTX_SETUP_STEPS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
function uniqueSetupSteps(steps: unknown): KtxSetupStep[] {
|
||||
if (!Array.isArray(steps)) {
|
||||
return [];
|
||||
}
|
||||
return [...new Set(steps.filter(isKtxSetupStep))];
|
||||
}
|
||||
|
||||
export function ktxSetupStatePath(projectDir: string): string {
|
||||
return join(projectDir, '.ktx', 'setup', 'state.json');
|
||||
}
|
||||
|
||||
export async function readKtxSetupState(projectDir: string): Promise<KtxSetupState> {
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(ktxSetupStatePath(projectDir), 'utf-8')) as Record<string, unknown>;
|
||||
return { completed_steps: uniqueSetupSteps(parsed.completed_steps) };
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
return { completed_steps: [] };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeKtxSetupState(projectDir: string, state: KtxSetupState): Promise<void> {
|
||||
await mkdir(join(projectDir, '.ktx', 'setup'), { recursive: true });
|
||||
await writeFile(
|
||||
ktxSetupStatePath(projectDir),
|
||||
`${JSON.stringify({ completed_steps: uniqueSetupSteps(state.completed_steps) }, null, 2)}\n`,
|
||||
'utf-8',
|
||||
);
|
||||
}
|
||||
|
||||
export async function markKtxSetupStateStepComplete(projectDir: string, step: KtxSetupStep): Promise<KtxSetupState> {
|
||||
const state = await readKtxSetupState(projectDir);
|
||||
const completedSteps = state.completed_steps.includes(step) ? state.completed_steps : [...state.completed_steps, step];
|
||||
const nextState = { completed_steps: completedSteps };
|
||||
await writeKtxSetupState(projectDir, nextState);
|
||||
return nextState;
|
||||
}
|
||||
|
||||
export function ktxSetupCompletedSteps(config: KtxProjectConfig, state: KtxSetupState): KtxSetupStep[] {
|
||||
return uniqueSetupSteps([...(config.setup?.completed_steps ?? []), ...state.completed_steps]);
|
||||
}
|
||||
|
||||
export function stripKtxSetupCompletedSteps(config: KtxProjectConfig): KtxProjectConfig {
|
||||
if (!config.setup) {
|
||||
return config;
|
||||
}
|
||||
const databaseConnectionIds = config.setup.database_connection_ids ?? [];
|
||||
if (databaseConnectionIds.length === 0) {
|
||||
const { setup: _setup, ...withoutSetup } = config;
|
||||
return withoutSetup;
|
||||
}
|
||||
return {
|
||||
...config,
|
||||
setup: {
|
||||
database_connection_ids: [...databaseConnectionIds],
|
||||
completed_steps: completedSteps.includes(step) ? [...completedSteps] : [...completedSteps, step],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -29,20 +88,14 @@ export function markKtxSetupStepComplete(config: KtxProjectConfig, step: KtxSetu
|
|||
export function setKtxSetupDatabaseConnectionIds(
|
||||
config: KtxProjectConfig,
|
||||
connectionIds: string[],
|
||||
options: { complete?: boolean } = {},
|
||||
): KtxProjectConfig {
|
||||
const uniqueConnectionIds = [...new Set(connectionIds.filter((connectionId) => connectionId.trim().length > 0))];
|
||||
const completedSteps = config.setup?.completed_steps ?? [];
|
||||
const nextCompletedSteps =
|
||||
options.complete === true && !completedSteps.includes('databases')
|
||||
? [...completedSteps, 'databases']
|
||||
: [...completedSteps];
|
||||
|
||||
return {
|
||||
...config,
|
||||
setup: {
|
||||
database_connection_ids: uniqueConnectionIds,
|
||||
completed_steps: nextCompletedSteps,
|
||||
...(config.setup?.completed_steps ? { completed_steps: [...config.setup.completed_steps] } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue