From 85932bb6d2e29d035913d25fc0de69a862ea404b Mon Sep 17 00:00:00 2001 From: Jonathan Jayet Date: Wed, 4 Feb 2026 10:45:11 +0100 Subject: [PATCH 1/3] feat: add csharp as a valid language for init --- src/index.ts | 2 +- src/mcp/metadata.ts | 2 +- src/utils/config.ts | 5 ++++- test/utils/config.test.ts | 12 ++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 31be0e0..2a2361e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ program .description("Initialize dora in the current repository") .option( "-l, --language ", - "Project language (typescript, javascript, python, rust, go, java)", + "Project language (typescript, javascript, python, rust, go, java, csharp)", ) .action( wrapCommand(async (options) => { diff --git a/src/mcp/metadata.ts b/src/mcp/metadata.ts index 3c32dbc..34e1090 100644 --- a/src/mcp/metadata.ts +++ b/src/mcp/metadata.ts @@ -25,7 +25,7 @@ export const toolsMetadata: ToolMetadata[] = [ name: "language", type: "string", description: - "Project language (typescript, javascript, python, rust, go, java)", + "Project language (typescript, javascript, python, rust, go, java, csharp)", required: false, }, ], diff --git a/src/utils/config.ts b/src/utils/config.ts index ad9e88f..ef16681 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "fs"; import { join } from "path"; -import { ZodError, z } from "zod"; +import { z } from "zod"; import { CtxError } from "./errors.ts"; import { findRepoRoot, @@ -29,6 +29,7 @@ export const LanguageSchema = z.enum([ "rust", "go", "java", + "csharp", ]); const ConfigSchema = z.object({ @@ -197,6 +198,8 @@ function detectIndexerCommand(params: { return "scip-go --output .dora/index.scip"; case "java": return "scip-java index --output .dora/index.scip"; + case "csharp": + return "scip-csharp index --output .dora/index.scip"; default: return "scip-typescript index --output .dora/index.scip"; } diff --git a/test/utils/config.test.ts b/test/utils/config.test.ts index 0756952..9fe0347 100644 --- a/test/utils/config.test.ts +++ b/test/utils/config.test.ts @@ -288,6 +288,18 @@ describe("Config Management", () => { ); }); + test("should use csharp indexer when language is csharp", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "csharp", + }); + + expect(config.language).toBe("csharp"); + expect(config.commands?.index).toBe( + "scip-csharp index --output .dora/index.scip", + ); + }); + test("should use go indexer when language is go", async () => { const config = createDefaultConfig({ root: tempDir, From 80800ba111ede8ba4d56f8d973e84b3850183c12 Mon Sep 17 00:00:00 2001 From: Jonathan Jayet Date: Wed, 4 Feb 2026 10:57:03 +0100 Subject: [PATCH 2/3] feat: adds all languages --- src/index.ts | 2 +- src/mcp/metadata.ts | 2 +- src/utils/config.ts | 489 ++++++++++++++++++++------------------ test/utils/config.test.ts | 96 ++++++++ 4 files changed, 352 insertions(+), 237 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a2361e..6b1fa1e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,7 @@ program .description("Initialize dora in the current repository") .option( "-l, --language ", - "Project language (typescript, javascript, python, rust, go, java, csharp)", + "Project language (typescript, javascript, python, rust, go, java, scala, kotlin, dart, ruby, c, cpp, php, csharp, visualbasic)", ) .action( wrapCommand(async (options) => { diff --git a/src/mcp/metadata.ts b/src/mcp/metadata.ts index 34e1090..138bd3d 100644 --- a/src/mcp/metadata.ts +++ b/src/mcp/metadata.ts @@ -25,7 +25,7 @@ export const toolsMetadata: ToolMetadata[] = [ name: "language", type: "string", description: - "Project language (typescript, javascript, python, rust, go, java, csharp)", + "Project language (typescript, javascript, python, rust, go, java, scala, kotlin, dart, ruby, c, cpp, php, csharp, visualbasic)", required: false, }, ], diff --git a/src/utils/config.ts b/src/utils/config.ts index ef16681..92cc3e0 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -5,46 +5,56 @@ import { join } from "path"; import { z } from "zod"; import { CtxError } from "./errors.ts"; import { - findRepoRoot, - getConfigPath, - getDoraDir, - resolveAbsolute, + findRepoRoot, + getConfigPath, + getDoraDir, + resolveAbsolute, } from "./paths.ts"; // Zod schemas for configuration validation const IndexStateSchema = z.object({ - gitCommit: z.string(), - gitHasUncommitted: z.boolean(), - fileCount: z.number(), - symbolCount: z.number(), - scipMtime: z.number(), - databaseMtime: z.number(), + gitCommit: z.string(), + gitHasUncommitted: z.boolean(), + fileCount: z.number(), + symbolCount: z.number(), + scipMtime: z.number(), + databaseMtime: z.number(), }); export const LanguageSchema = z.enum([ - "typescript", - "javascript", - "python", - "rust", - "go", - "java", + "typescript", + "javascript", + "python", + "rust", + "go", + "java", + "scala", + "kotlin", + "dart", + "ruby", + "c", + "cpp", + "php", "csharp", + "visualbasic", ]); +export type Language = z.infer; + const ConfigSchema = z.object({ - root: z.string().min(1), - scip: z.string().min(1), - db: z.string().min(1), - language: LanguageSchema.optional(), - commands: z - .object({ - index: z.string().optional(), - }) - .optional(), - lastIndexed: z.string().nullable(), - indexState: IndexStateSchema.optional(), - ignore: z.array(z.string()).optional(), + root: z.string().min(1), + scip: z.string().min(1), + db: z.string().min(1), + language: LanguageSchema.optional(), + commands: z + .object({ + index: z.string().optional(), + }) + .optional(), + lastIndexed: z.string().nullable(), + indexState: IndexStateSchema.optional(), + ignore: z.array(z.string()).optional(), }); // Export types inferred from schemas @@ -55,256 +65,265 @@ export type Config = z.infer; * Load configuration from .dora/config.json */ export async function loadConfig(root?: string): Promise { - if (!root) { - root = await findRepoRoot(); - } - - const configPath = getConfigPath(root); - - if (!existsSync(configPath)) { - throw new CtxError( - `No config found. Run 'dora init' first to initialize the repository.`, - ); - } - - try { - const file = Bun.file(configPath); - const data = await file.json(); - return validateConfig(data); - } catch (error) { - throw new CtxError( - `Failed to read config: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } + if (!root) { + root = await findRepoRoot(); + } + + const configPath = getConfigPath(root); + + if (!existsSync(configPath)) { + throw new CtxError( + `No config found. Run 'dora init' first to initialize the repository.`, + ); + } + + try { + const file = Bun.file(configPath); + const data = await file.json(); + return validateConfig(data); + } catch (error) { + throw new CtxError( + `Failed to read config: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } } /** * Save configuration to .dora/config.json */ export async function saveConfig(config: Config): Promise { - const configPath = getConfigPath(config.root); - - try { - await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n"); - } catch (error) { - throw new CtxError( - `Failed to write config: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } + const configPath = getConfigPath(config.root); + + try { + await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n"); + } catch (error) { + throw new CtxError( + `Failed to write config: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } } /** * Validate configuration object using Zod schema */ export function validateConfig(data: unknown): Config { - const result = ConfigSchema.safeParse(data); - - if (!result.success) { - // Convert Zod errors to more user-friendly messages - const firstError = result.error.issues[0]; - if (!firstError) { - throw new CtxError("Invalid config: unknown validation error"); - } - const field = firstError.path.join("."); - throw new CtxError( - `Invalid config: ${field ? `field '${field}' ` : ""}${firstError.message}`, - ); - } - - return result.data; + const result = ConfigSchema.safeParse(data); + + if (!result.success) { + // Convert Zod errors to more user-friendly messages + const firstError = result.error.issues[0]; + if (!firstError) { + throw new CtxError("Invalid config: unknown validation error"); + } + const field = firstError.path.join("."); + throw new CtxError( + `Invalid config: ${field ? `field '${field}' ` : ""}${ + firstError.message + }`, + ); + } + + return result.data; } /** * Detect package manager and workspace type based on lock files */ function detectWorkspaceType(root: string): "bun" | "pnpm" | "yarn" | null { - // Check for Bun first (bun.lockb) - if (existsSync(join(root, "bun.lockb"))) { - return "bun"; - } - - // Check for pnpm (pnpm-lock.yaml or pnpm-workspace.yaml) - if ( - existsSync(join(root, "pnpm-lock.yaml")) || - existsSync(join(root, "pnpm-workspace.yaml")) - ) { - return "pnpm"; - } - - // Check for Yarn (yarn.lock) - if (existsSync(join(root, "yarn.lock"))) { - return "yarn"; - } - - // Check for yarn workspaces in package.json as fallback - const packageJsonPath = join(root, "package.json"); - if (existsSync(packageJsonPath)) { - try { - const content = readFileSync(packageJsonPath, "utf-8"); - const packageJson = JSON.parse(content); - if (packageJson.workspaces) { - return "yarn"; - } - } catch {} - } - - return null; + // Check for Bun first (bun.lockb) + if (existsSync(join(root, "bun.lockb"))) { + return "bun"; + } + + // Check for pnpm (pnpm-lock.yaml or pnpm-workspace.yaml) + if ( + existsSync(join(root, "pnpm-lock.yaml")) || + existsSync(join(root, "pnpm-workspace.yaml")) + ) { + return "pnpm"; + } + + // Check for Yarn (yarn.lock) + if (existsSync(join(root, "yarn.lock"))) { + return "yarn"; + } + + // Check for yarn workspaces in package.json as fallback + const packageJsonPath = join(root, "package.json"); + if (existsSync(packageJsonPath)) { + try { + const content = readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(content); + if (packageJson.workspaces) { + return "yarn"; + } + } catch {} + } + + return null; } /** * Detect project type and return appropriate SCIP indexer command */ function detectIndexerCommand(params: { - root: string; - language?: string; + root: string; + language?: string; }): string { - const { root, language } = params; - - if (language) { - switch (language) { - case "typescript": - case "javascript": { - const workspaceType = detectWorkspaceType(root); - const needsInferTsConfig = language === "javascript"; - - if (workspaceType === "bun") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - if (workspaceType === "pnpm") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" - : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; - } - if (workspaceType === "yarn") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" - : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; - } - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - case "python": - return "scip-python index --output .dora/index.scip"; - case "rust": - return "rust-analyzer scip . --output .dora/index.scip"; - case "go": - return "scip-go --output .dora/index.scip"; - case "java": - return "scip-java index --output .dora/index.scip"; + const { root, language } = params; + + if (language) { + switch (language) { + case "typescript": + case "javascript": { + const workspaceType = detectWorkspaceType(root); + const needsInferTsConfig = language === "javascript"; + + if (workspaceType === "bun") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + if (workspaceType === "pnpm") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" + : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; + } + if (workspaceType === "yarn") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" + : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; + } + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + case "python": + return "scip-python index --output .dora/index.scip"; + case "rust": + return "rust-analyzer scip . --output .dora/index.scip"; + case "go": + return "scip-go --output .dora/index.scip"; + case "java": + case "scala": + case "kotlin": + return "scip-java index --output .dora/index.scip"; + case "dart": + return "scip-dart index --output .dora/index.scip"; + case "ruby": + return "scip-ruby index --output .dora/index.scip"; + case "c": + case "cpp": + return "scip-clang index --output .dora/index.scip"; + case "php": + return "scip-php index --output .dora/index.scip"; case "csharp": - return "scip-csharp index --output .dora/index.scip"; - default: - return "scip-typescript index --output .dora/index.scip"; - } - } - - const hasTsConfig = existsSync(join(root, "tsconfig.json")); - const hasPackageJson = existsSync(join(root, "package.json")); - - if (hasTsConfig || hasPackageJson) { - const workspaceType = detectWorkspaceType(root); - - const needsInferTsConfig = !hasTsConfig && hasPackageJson; - - if (workspaceType === "bun") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - if (workspaceType === "pnpm") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" - : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; - } - if (workspaceType === "yarn") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" - : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; - } - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - - if ( - existsSync(join(root, "setup.py")) || - existsSync(join(root, "pyproject.toml")) || - existsSync(join(root, "requirements.txt")) - ) { - return "scip-python index --output .dora/index.scip"; - } - - if (existsSync(join(root, "Cargo.toml"))) { - return "rust-analyzer scip . --output .dora/index.scip"; - } - - if (existsSync(join(root, "go.mod"))) { - return "scip-go --output .dora/index.scip"; - } - - if ( - existsSync(join(root, "pom.xml")) || - existsSync(join(root, "build.gradle")) || - existsSync(join(root, "build.gradle.kts")) - ) { - return "scip-java index --output .dora/index.scip"; - } - - return "scip-typescript index --output .dora/index.scip"; + case "visualbasic": + return "scip-csharp index --output .dora/index.scip"; + default: + return "scip-typescript index --output .dora/index.scip"; + } + } + + const hasTsConfig = existsSync(join(root, "tsconfig.json")); + const hasPackageJson = existsSync(join(root, "package.json")); + + if (hasTsConfig || hasPackageJson) { + const workspaceType = detectWorkspaceType(root); + + const needsInferTsConfig = !hasTsConfig && hasPackageJson; + + if (workspaceType === "bun") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + if (workspaceType === "pnpm") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" + : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; + } + if (workspaceType === "yarn") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" + : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; + } + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + + if ( + existsSync(join(root, "setup.py")) || + existsSync(join(root, "pyproject.toml")) || + existsSync(join(root, "requirements.txt")) + ) { + return "scip-python index --output .dora/index.scip"; + } + + if (existsSync(join(root, "Cargo.toml"))) { + return "rust-analyzer scip . --output .dora/index.scip"; + } + + if (existsSync(join(root, "go.mod"))) { + return "scip-go --output .dora/index.scip"; + } + + if ( + existsSync(join(root, "pom.xml")) || + existsSync(join(root, "build.gradle")) || + existsSync(join(root, "build.gradle.kts")) + ) { + return "scip-java index --output .dora/index.scip"; + } + + return "scip-typescript index --output .dora/index.scip"; } /** * Create default configuration */ export function createDefaultConfig(params: { - root: string; - language?: string; + root: string; + language?: string; }): Config { - const indexCommand = detectIndexerCommand({ - root: params.root, - language: params.language, - }); - - return { - root: params.root, - scip: ".dora/index.scip", - db: ".dora/dora.db", - language: params.language as - | "typescript" - | "javascript" - | "python" - | "rust" - | "go" - | "java" - | undefined, - commands: { - index: indexCommand, - }, - lastIndexed: null, - }; + const indexCommand = detectIndexerCommand({ + root: params.root, + language: params.language, + }); + + return { + root: params.root, + scip: ".dora/index.scip", + db: ".dora/dora.db", + language: params.language as + | Language + | undefined, + commands: { + index: indexCommand, + }, + lastIndexed: null, + }; } /** * Check if repository is initialized (has .dora directory) */ export function isInitialized(root: string): boolean { - return existsSync(getDoraDir(root)); + return existsSync(getDoraDir(root)); } /** * Check if repository is indexed (has database file) */ export async function isIndexed(config: Config): Promise { - const dbPath = resolveAbsolute({ - root: config.root, - relativePath: config.db, - }); - return existsSync(dbPath); + const dbPath = resolveAbsolute({ + root: config.root, + relativePath: config.db, + }); + return existsSync(dbPath); } diff --git a/test/utils/config.test.ts b/test/utils/config.test.ts index 9fe0347..ed775e8 100644 --- a/test/utils/config.test.ts +++ b/test/utils/config.test.ts @@ -300,6 +300,102 @@ describe("Config Management", () => { ); }); + test("should use csharp indexer when language is visualbasic", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "visualbasic", + }); + + expect(config.language).toBe("visualbasic"); + expect(config.commands?.index).toBe( + "scip-csharp index --output .dora/index.scip", + ); + }); + + test("should use java indexer when language is java", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "java", + }); + + expect(config.language).toBe("java"); + expect(config.commands?.index).toBe( + "scip-java index --output .dora/index.scip", + ); + }); + + test("should use java indexer when language is kotlin", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "kotlin", + }); + + expect(config.language).toBe("kotlin"); + expect(config.commands?.index).toBe( + "scip-java index --output .dora/index.scip", + ); + }); + + test("should use java indexer when language is scala", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "scala", + }); + + expect(config.language).toBe("scala"); + expect(config.commands?.index).toBe( + "scip-java index --output .dora/index.scip", + ); + }); + + test("should use ruby indexer when language is ruby", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "ruby", + }); + + expect(config.language).toBe("ruby"); + expect(config.commands?.index).toBe( + "scip-ruby index --output .dora/index.scip", + ); + }); + + test("should use dart indexer when language is dart", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "dart", + }); + + expect(config.language).toBe("dart"); + expect(config.commands?.index).toBe( + "scip-dart index --output .dora/index.scip", + ); + }); + + test("should use clang indexer when language is c", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "c", + }); + + expect(config.language).toBe("c"); + expect(config.commands?.index).toBe( + "scip-clang index --output .dora/index.scip", + ); + }); + + test("should use clang indexer when language is cpp", async () => { + const config = createDefaultConfig({ + root: tempDir, + language: "cpp", + }); + + expect(config.language).toBe("cpp"); + expect(config.commands?.index).toBe( + "scip-clang index --output .dora/index.scip", + ); + }); + test("should use go indexer when language is go", async () => { const config = createDefaultConfig({ root: tempDir, From 462f171dd35ab6fd5ac6709e81a29ef4d36c5253 Mon Sep 17 00:00:00 2001 From: Jonathan Jayet Date: Thu, 5 Feb 2026 10:16:01 +0100 Subject: [PATCH 3/3] fix: linter --- src/utils/config.ts | 506 ++++++++++++++++++++++---------------------- 1 file changed, 252 insertions(+), 254 deletions(-) diff --git a/src/utils/config.ts b/src/utils/config.ts index 92cc3e0..e935536 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -5,56 +5,56 @@ import { join } from "path"; import { z } from "zod"; import { CtxError } from "./errors.ts"; import { - findRepoRoot, - getConfigPath, - getDoraDir, - resolveAbsolute, + findRepoRoot, + getConfigPath, + getDoraDir, + resolveAbsolute, } from "./paths.ts"; // Zod schemas for configuration validation const IndexStateSchema = z.object({ - gitCommit: z.string(), - gitHasUncommitted: z.boolean(), - fileCount: z.number(), - symbolCount: z.number(), - scipMtime: z.number(), - databaseMtime: z.number(), + gitCommit: z.string(), + gitHasUncommitted: z.boolean(), + fileCount: z.number(), + symbolCount: z.number(), + scipMtime: z.number(), + databaseMtime: z.number(), }); export const LanguageSchema = z.enum([ - "typescript", - "javascript", - "python", - "rust", - "go", - "java", - "scala", - "kotlin", - "dart", - "ruby", - "c", - "cpp", - "php", - "csharp", - "visualbasic", + "typescript", + "javascript", + "python", + "rust", + "go", + "java", + "scala", + "kotlin", + "dart", + "ruby", + "c", + "cpp", + "php", + "csharp", + "visualbasic", ]); export type Language = z.infer; const ConfigSchema = z.object({ - root: z.string().min(1), - scip: z.string().min(1), - db: z.string().min(1), - language: LanguageSchema.optional(), - commands: z - .object({ - index: z.string().optional(), - }) - .optional(), - lastIndexed: z.string().nullable(), - indexState: IndexStateSchema.optional(), - ignore: z.array(z.string()).optional(), + root: z.string().min(1), + scip: z.string().min(1), + db: z.string().min(1), + language: LanguageSchema.optional(), + commands: z + .object({ + index: z.string().optional(), + }) + .optional(), + lastIndexed: z.string().nullable(), + indexState: IndexStateSchema.optional(), + ignore: z.array(z.string()).optional(), }); // Export types inferred from schemas @@ -65,265 +65,263 @@ export type Config = z.infer; * Load configuration from .dora/config.json */ export async function loadConfig(root?: string): Promise { - if (!root) { - root = await findRepoRoot(); - } - - const configPath = getConfigPath(root); - - if (!existsSync(configPath)) { - throw new CtxError( - `No config found. Run 'dora init' first to initialize the repository.`, - ); - } - - try { - const file = Bun.file(configPath); - const data = await file.json(); - return validateConfig(data); - } catch (error) { - throw new CtxError( - `Failed to read config: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } + if (!root) { + root = await findRepoRoot(); + } + + const configPath = getConfigPath(root); + + if (!existsSync(configPath)) { + throw new CtxError( + `No config found. Run 'dora init' first to initialize the repository.`, + ); + } + + try { + const file = Bun.file(configPath); + const data = await file.json(); + return validateConfig(data); + } catch (error) { + throw new CtxError( + `Failed to read config: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } } /** * Save configuration to .dora/config.json */ export async function saveConfig(config: Config): Promise { - const configPath = getConfigPath(config.root); - - try { - await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n"); - } catch (error) { - throw new CtxError( - `Failed to write config: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } + const configPath = getConfigPath(config.root); + + try { + await Bun.write(configPath, JSON.stringify(config, null, 2) + "\n"); + } catch (error) { + throw new CtxError( + `Failed to write config: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } } /** * Validate configuration object using Zod schema */ export function validateConfig(data: unknown): Config { - const result = ConfigSchema.safeParse(data); - - if (!result.success) { - // Convert Zod errors to more user-friendly messages - const firstError = result.error.issues[0]; - if (!firstError) { - throw new CtxError("Invalid config: unknown validation error"); - } - const field = firstError.path.join("."); - throw new CtxError( - `Invalid config: ${field ? `field '${field}' ` : ""}${ - firstError.message - }`, - ); - } - - return result.data; + const result = ConfigSchema.safeParse(data); + + if (!result.success) { + // Convert Zod errors to more user-friendly messages + const firstError = result.error.issues[0]; + if (!firstError) { + throw new CtxError("Invalid config: unknown validation error"); + } + const field = firstError.path.join("."); + throw new CtxError( + `Invalid config: ${field ? `field '${field}' ` : ""}${ + firstError.message + }`, + ); + } + + return result.data; } /** * Detect package manager and workspace type based on lock files */ function detectWorkspaceType(root: string): "bun" | "pnpm" | "yarn" | null { - // Check for Bun first (bun.lockb) - if (existsSync(join(root, "bun.lockb"))) { - return "bun"; - } - - // Check for pnpm (pnpm-lock.yaml or pnpm-workspace.yaml) - if ( - existsSync(join(root, "pnpm-lock.yaml")) || - existsSync(join(root, "pnpm-workspace.yaml")) - ) { - return "pnpm"; - } - - // Check for Yarn (yarn.lock) - if (existsSync(join(root, "yarn.lock"))) { - return "yarn"; - } - - // Check for yarn workspaces in package.json as fallback - const packageJsonPath = join(root, "package.json"); - if (existsSync(packageJsonPath)) { - try { - const content = readFileSync(packageJsonPath, "utf-8"); - const packageJson = JSON.parse(content); - if (packageJson.workspaces) { - return "yarn"; - } - } catch {} - } - - return null; + // Check for Bun first (bun.lockb) + if (existsSync(join(root, "bun.lockb"))) { + return "bun"; + } + + // Check for pnpm (pnpm-lock.yaml or pnpm-workspace.yaml) + if ( + existsSync(join(root, "pnpm-lock.yaml")) || + existsSync(join(root, "pnpm-workspace.yaml")) + ) { + return "pnpm"; + } + + // Check for Yarn (yarn.lock) + if (existsSync(join(root, "yarn.lock"))) { + return "yarn"; + } + + // Check for yarn workspaces in package.json as fallback + const packageJsonPath = join(root, "package.json"); + if (existsSync(packageJsonPath)) { + try { + const content = readFileSync(packageJsonPath, "utf-8"); + const packageJson = JSON.parse(content); + if (packageJson.workspaces) { + return "yarn"; + } + } catch {} + } + + return null; } /** * Detect project type and return appropriate SCIP indexer command */ function detectIndexerCommand(params: { - root: string; - language?: string; + root: string; + language?: string; }): string { - const { root, language } = params; - - if (language) { - switch (language) { - case "typescript": - case "javascript": { - const workspaceType = detectWorkspaceType(root); - const needsInferTsConfig = language === "javascript"; - - if (workspaceType === "bun") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - if (workspaceType === "pnpm") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" - : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; - } - if (workspaceType === "yarn") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" - : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; - } - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - case "python": - return "scip-python index --output .dora/index.scip"; - case "rust": - return "rust-analyzer scip . --output .dora/index.scip"; - case "go": - return "scip-go --output .dora/index.scip"; - case "java": - case "scala": - case "kotlin": - return "scip-java index --output .dora/index.scip"; - case "dart": - return "scip-dart index --output .dora/index.scip"; - case "ruby": - return "scip-ruby index --output .dora/index.scip"; - case "c": - case "cpp": - return "scip-clang index --output .dora/index.scip"; - case "php": - return "scip-php index --output .dora/index.scip"; - case "csharp": - case "visualbasic": - return "scip-csharp index --output .dora/index.scip"; - default: - return "scip-typescript index --output .dora/index.scip"; - } - } - - const hasTsConfig = existsSync(join(root, "tsconfig.json")); - const hasPackageJson = existsSync(join(root, "package.json")); - - if (hasTsConfig || hasPackageJson) { - const workspaceType = detectWorkspaceType(root); - - const needsInferTsConfig = !hasTsConfig && hasPackageJson; - - if (workspaceType === "bun") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - if (workspaceType === "pnpm") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" - : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; - } - if (workspaceType === "yarn") { - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" - : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; - } - return needsInferTsConfig - ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" - : "scip-typescript index --output .dora/index.scip"; - } - - if ( - existsSync(join(root, "setup.py")) || - existsSync(join(root, "pyproject.toml")) || - existsSync(join(root, "requirements.txt")) - ) { - return "scip-python index --output .dora/index.scip"; - } - - if (existsSync(join(root, "Cargo.toml"))) { - return "rust-analyzer scip . --output .dora/index.scip"; - } - - if (existsSync(join(root, "go.mod"))) { - return "scip-go --output .dora/index.scip"; - } - - if ( - existsSync(join(root, "pom.xml")) || - existsSync(join(root, "build.gradle")) || - existsSync(join(root, "build.gradle.kts")) - ) { - return "scip-java index --output .dora/index.scip"; - } - - return "scip-typescript index --output .dora/index.scip"; + const { root, language } = params; + + if (language) { + switch (language) { + case "typescript": + case "javascript": { + const workspaceType = detectWorkspaceType(root); + const needsInferTsConfig = language === "javascript"; + + if (workspaceType === "bun") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + if (workspaceType === "pnpm") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" + : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; + } + if (workspaceType === "yarn") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" + : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; + } + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + case "python": + return "scip-python index --output .dora/index.scip"; + case "rust": + return "rust-analyzer scip . --output .dora/index.scip"; + case "go": + return "scip-go --output .dora/index.scip"; + case "java": + case "scala": + case "kotlin": + return "scip-java index --output .dora/index.scip"; + case "dart": + return "scip-dart index --output .dora/index.scip"; + case "ruby": + return "scip-ruby index --output .dora/index.scip"; + case "c": + case "cpp": + return "scip-clang index --output .dora/index.scip"; + case "php": + return "scip-php index --output .dora/index.scip"; + case "csharp": + case "visualbasic": + return "scip-csharp index --output .dora/index.scip"; + default: + return "scip-typescript index --output .dora/index.scip"; + } + } + + const hasTsConfig = existsSync(join(root, "tsconfig.json")); + const hasPackageJson = existsSync(join(root, "package.json")); + + if (hasTsConfig || hasPackageJson) { + const workspaceType = detectWorkspaceType(root); + + const needsInferTsConfig = !hasTsConfig && hasPackageJson; + + if (workspaceType === "bun") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + if (workspaceType === "pnpm") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --pnpm-workspaces --output .dora/index.scip" + : "scip-typescript index --pnpm-workspaces --output .dora/index.scip"; + } + if (workspaceType === "yarn") { + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --yarn-workspaces --output .dora/index.scip" + : "scip-typescript index --yarn-workspaces --output .dora/index.scip"; + } + return needsInferTsConfig + ? "scip-typescript index --infer-tsconfig --output .dora/index.scip" + : "scip-typescript index --output .dora/index.scip"; + } + + if ( + existsSync(join(root, "setup.py")) || + existsSync(join(root, "pyproject.toml")) || + existsSync(join(root, "requirements.txt")) + ) { + return "scip-python index --output .dora/index.scip"; + } + + if (existsSync(join(root, "Cargo.toml"))) { + return "rust-analyzer scip . --output .dora/index.scip"; + } + + if (existsSync(join(root, "go.mod"))) { + return "scip-go --output .dora/index.scip"; + } + + if ( + existsSync(join(root, "pom.xml")) || + existsSync(join(root, "build.gradle")) || + existsSync(join(root, "build.gradle.kts")) + ) { + return "scip-java index --output .dora/index.scip"; + } + + return "scip-typescript index --output .dora/index.scip"; } /** * Create default configuration */ export function createDefaultConfig(params: { - root: string; - language?: string; + root: string; + language?: string; }): Config { - const indexCommand = detectIndexerCommand({ - root: params.root, - language: params.language, - }); - - return { - root: params.root, - scip: ".dora/index.scip", - db: ".dora/dora.db", - language: params.language as - | Language - | undefined, - commands: { - index: indexCommand, - }, - lastIndexed: null, - }; + const indexCommand = detectIndexerCommand({ + root: params.root, + language: params.language, + }); + + return { + root: params.root, + scip: ".dora/index.scip", + db: ".dora/dora.db", + language: params.language as Language | undefined, + commands: { + index: indexCommand, + }, + lastIndexed: null, + }; } /** * Check if repository is initialized (has .dora directory) */ export function isInitialized(root: string): boolean { - return existsSync(getDoraDir(root)); + return existsSync(getDoraDir(root)); } /** * Check if repository is indexed (has database file) */ export async function isIndexed(config: Config): Promise { - const dbPath = resolveAbsolute({ - root: config.root, - relativePath: config.db, - }); - return existsSync(dbPath); + const dbPath = resolveAbsolute({ + root: config.root, + relativePath: config.db, + }); + return existsSync(dbPath); }