ktx/packages/context/src/project/project.ts

147 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-05-10 23:12:26 +02:00
import { promises as fs } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
2026-05-10 23:51:24 +02:00
import { GitService, type KtxCoreConfig, type KtxLogger, noopLogger } from '../core/index.js';
import type { KtxProjectConfig } from './config.js';
import { buildDefaultKtxProjectConfig, parseKtxProjectConfig, serializeKtxProjectConfig } from './config.js';
2026-05-10 23:12:26 +02:00
import { LocalGitFileStore } from './local-git-file-store.js';
2026-05-10 23:51:24 +02:00
export interface InitKtxProjectOptions {
2026-05-10 23:12:26 +02:00
projectDir: string;
projectName?: string;
force?: boolean;
authorName?: string;
authorEmail?: string;
2026-05-10 23:51:24 +02:00
logger?: KtxLogger;
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
export interface LoadKtxProjectOptions {
2026-05-10 23:12:26 +02:00
projectDir: string;
authorName?: string;
authorEmail?: string;
2026-05-10 23:51:24 +02:00
logger?: KtxLogger;
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
export interface KtxLocalProject {
2026-05-10 23:12:26 +02:00
projectDir: string;
configPath: string;
2026-05-10 23:51:24 +02:00
config: KtxProjectConfig;
coreConfig: KtxCoreConfig;
2026-05-10 23:12:26 +02:00
git: GitService;
fileStore: LocalGitFileStore;
}
2026-05-10 23:51:24 +02:00
export interface InitKtxProjectResult extends KtxLocalProject {
2026-05-10 23:12:26 +02:00
commitHash: string | null;
}
const TRACKED_SCAFFOLD_FILES: Array<{ path: string; content: string }> = [
2026-05-11 00:31:15 -07:00
{
path: '.ktx/.gitignore',
content: 'cache/\ndb.sqlite\ndb.sqlite-*\ningest-transcripts/\nsecrets/\nsetup/\nagents/\n',
},
2026-05-10 23:51:24 +02:00
{ path: '.ktx/prompts/.gitkeep', content: '' },
{ path: '.ktx/skills/.gitkeep', content: '' },
2026-05-10 23:12:26 +02:00
{ path: 'knowledge/global/.gitkeep', content: '' },
{ path: 'semantic-layer/.gitkeep', content: '' },
{ path: 'raw-sources/.gitkeep', content: '' },
];
2026-05-10 23:51:24 +02:00
function createCoreConfig(projectDir: string, authorName: string, authorEmail: string): KtxCoreConfig {
2026-05-10 23:12:26 +02:00
return {
storage: {
configDir: projectDir,
homeDir: dirname(projectDir),
2026-05-10 23:51:24 +02:00
worktreesDir: join(projectDir, '.ktx/worktrees'),
2026-05-10 23:12:26 +02:00
},
git: {
userName: authorName,
userEmail: authorEmail,
2026-05-10 23:51:24 +02:00
bootstrapMessage: 'Initialize ktx project repository',
2026-05-10 23:12:26 +02:00
bootstrapAuthor: authorName,
bootstrapAuthorEmail: authorEmail,
},
};
}
async function fileExists(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
async function writeProjectFile(projectDir: string, relativePath: string, content: string): Promise<void> {
const absolutePath = join(projectDir, relativePath);
await fs.mkdir(dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, content, 'utf-8');
}
async function createRuntime(
projectDir: string,
2026-05-10 23:51:24 +02:00
config: KtxProjectConfig,
2026-05-10 23:12:26 +02:00
authorName: string,
authorEmail: string,
2026-05-10 23:51:24 +02:00
logger: KtxLogger,
): Promise<KtxLocalProject> {
2026-05-10 23:12:26 +02:00
const coreConfig = createCoreConfig(projectDir, authorName, authorEmail);
const git = new GitService(coreConfig, logger);
await git.onModuleInit();
return {
projectDir,
2026-05-10 23:51:24 +02:00
configPath: join(projectDir, 'ktx.yaml'),
2026-05-10 23:12:26 +02:00
config,
coreConfig,
git,
fileStore: new LocalGitFileStore({ rootDir: projectDir, git }),
};
}
2026-05-10 23:51:24 +02:00
export async function initKtxProject(options: InitKtxProjectOptions): Promise<InitKtxProjectResult> {
2026-05-10 23:12:26 +02:00
const projectDir = resolve(options.projectDir);
2026-05-10 23:51:24 +02:00
const projectName = options.projectName?.trim() || basename(projectDir) || 'ktx-project';
const authorName = options.authorName ?? 'ktx';
const authorEmail = options.authorEmail ?? 'ktx@example.com';
2026-05-10 23:12:26 +02:00
const logger = options.logger ?? noopLogger;
2026-05-10 23:51:24 +02:00
const configPath = join(projectDir, 'ktx.yaml');
2026-05-10 23:12:26 +02:00
await fs.mkdir(projectDir, { recursive: true });
if (!options.force && (await fileExists(configPath))) {
2026-05-10 23:51:24 +02:00
throw new Error(`Project already contains ktx.yaml: ${configPath}`);
2026-05-10 23:12:26 +02:00
}
2026-05-10 23:51:24 +02:00
const config = buildDefaultKtxProjectConfig(projectName);
2026-05-10 23:12:26 +02:00
const runtime = await createRuntime(projectDir, config, authorName, authorEmail, logger);
2026-05-10 23:51:24 +02:00
await writeProjectFile(projectDir, 'ktx.yaml', serializeKtxProjectConfig(config));
await fs.mkdir(join(projectDir, '.ktx/cache'), { recursive: true });
2026-05-10 23:12:26 +02:00
for (const file of TRACKED_SCAFFOLD_FILES) {
await writeProjectFile(projectDir, file.path, file.content);
}
const commit = await runtime.git.commitFiles(
2026-05-10 23:51:24 +02:00
['ktx.yaml', ...TRACKED_SCAFFOLD_FILES.map((file) => file.path)],
`Initialize KTX project: ${projectName}`,
2026-05-10 23:12:26 +02:00
authorName,
authorEmail,
);
return {
...runtime,
commitHash: commit.commitHash,
};
}
2026-05-10 23:51:24 +02:00
export async function loadKtxProject(options: LoadKtxProjectOptions): Promise<KtxLocalProject> {
2026-05-10 23:12:26 +02:00
const projectDir = resolve(options.projectDir);
2026-05-10 23:51:24 +02:00
const authorName = options.authorName ?? 'ktx';
const authorEmail = options.authorEmail ?? 'ktx@example.com';
2026-05-10 23:12:26 +02:00
const logger = options.logger ?? noopLogger;
2026-05-10 23:51:24 +02:00
const configPath = join(projectDir, 'ktx.yaml');
2026-05-10 23:12:26 +02:00
const raw = await fs.readFile(configPath, 'utf-8');
2026-05-10 23:51:24 +02:00
const config = parseKtxProjectConfig(raw);
2026-05-10 23:12:26 +02:00
return createRuntime(projectDir, config, authorName, authorEmail, logger);
}